Skip to content
Merged
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


76 changes: 76 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 All @@ -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.
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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.
Expand Down
33 changes: 33 additions & 0 deletions Bindings/go/SRAL/sral.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
24 changes: 23 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 Expand Up @@ -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

Expand Down
51 changes: 47 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 All @@ -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) {
Expand Down Expand Up @@ -122,26 +131,47 @@ 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)");
}


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 +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();
Expand Down
80 changes: 80 additions & 0 deletions Include/SRAL.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
Loading
Loading