From cc69c42f9674fd32b17e156bde3e73471cacf993 Mon Sep 17 00:00:00 2001 From: Jesse Jurman Date: Mon, 16 Mar 2026 02:34:48 -0400 Subject: [PATCH 1/2] Prioritize VoiceOver, Cocoa Example --- Bindings/Python/sral.py | 4 +- Bindings/go/SRAL/types.go | 4 +- CMakeLists.txt | 9 +++ Examples/ObjC/SRALCocoaExample.m | 97 ++++++++++++++++++++++++++++++++ Include/SRAL.h | 8 +-- SRC/VoiceOver.mm | 2 + 6 files changed, 117 insertions(+), 7 deletions(-) create mode 100644 Examples/ObjC/SRALCocoaExample.m diff --git a/Bindings/Python/sral.py b/Bindings/Python/sral.py index 760e634..73d6732 100644 --- a/Bindings/Python/sral.py +++ b/Bindings/Python/sral.py @@ -1,6 +1,7 @@ from enum import IntEnum import ctypes import os +import sys # --- Load the SRAL C Library --- try: @@ -28,7 +29,8 @@ class SRALEngine(IntEnum): SAPI = 1 << 6 SPEECH_DISPATCHER = 1 << 7 VOICE_OVER = 1 << 8 - AV_SPEECH = 1 << 9 + NS_SPEECH = 1 << 9 + AV_SPEECH = 1 << 10 class SRALFeature(IntEnum): """ diff --git a/Bindings/go/SRAL/types.go b/Bindings/go/SRAL/types.go index f01c5fd..69a953b 100644 --- a/Bindings/go/SRAL/types.go +++ b/Bindings/go/SRAL/types.go @@ -21,10 +21,10 @@ const ( SAPIEngine // SpeechDispatcherEngine — Speech Dispatcher, a common daemon for Linux systems. SpeechDispatcherEngine - // NSSpeechEngine — Apple NSSpeechSynthesizer. - NSSpeechEngine // VoiceOverEngine — Apple VoiceOver, the built-in screen reader on Apple platforms. VoiceOverEngine + // NSSpeechEngine — Apple NSSpeechSynthesizer. + NSSpeechEngine // AVSpeechEngine — AVFoundation Speech Synthesizer (AVSpeechSynthesizer) for Apple platforms. AVSpeechEngine // AllEngines is a bitmask of all supported engines. diff --git a/CMakeLists.txt b/CMakeLists.txt index ecb1542..52c5d6c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -100,6 +100,7 @@ endif() target_link_libraries(${PROJECT_NAME}_static ${LIBS}) endif() elseif (APPLE) + enable_language(OBJC) enable_language(OBJCXX) set(CMAKE_C_COMPILER clang) set(CMAKE_CXX_COMPILER clang++) @@ -117,6 +118,14 @@ if (BUILD_SRAL_TEST) "-framework Foundation" "-framework AVFoundation" ) + + add_executable(${PROJECT_NAME}_test_cocoa "Examples/ObjC/SRALCocoaExample.m" "Include/SRAL.h") + target_link_libraries(${PROJECT_NAME}_test_cocoa + ${PROJECT_NAME}_static + "-framework AppKit" + "-framework Foundation" + "-framework AVFoundation" + ) endif() else() find_package(PkgConfig REQUIRED) diff --git a/Examples/ObjC/SRALCocoaExample.m b/Examples/ObjC/SRALCocoaExample.m new file mode 100644 index 0000000..d41c1b5 --- /dev/null +++ b/Examples/ObjC/SRALCocoaExample.m @@ -0,0 +1,97 @@ +#import + +#define SRAL_STATIC +#include + +@interface AppDelegate : NSObject +@property (strong) NSWindow *window; +@property (strong) NSTextField *engineLabel; +@property (strong) NSTextField *speakingLabel; +@property (strong) NSTimer *speakingTimer; +@end + +@implementation AppDelegate + +- (void)applicationDidFinishLaunching:(NSNotification *)notification { + NSRect frame = NSMakeRect(200, 200, 400, 200); + self.window = [[NSWindow alloc] + initWithContentRect:frame + styleMask:(NSWindowStyleMaskTitled | + NSWindowStyleMaskClosable | + NSWindowStyleMaskMiniaturizable) + backing:NSBackingStoreBuffered + defer:NO]; + [self.window setTitle:@"SRAL Cocoa Example"]; + + self.engineLabel = [NSTextField labelWithString:@"Engine: (initializing...)"]; + [self.engineLabel setFrame:NSMakeRect(20, 150, 360, 20)]; + [[self.window contentView] addSubview:self.engineLabel]; + + self.speakingLabel = [NSTextField labelWithString:@"Speaking: No"]; + [self.speakingLabel setFrame:NSMakeRect(20, 125, 360, 20)]; + [[self.window contentView] addSubview:self.speakingLabel]; + + NSButton *speakButton = [NSButton buttonWithTitle:@"Speak" + target:self + action:@selector(speakClicked:)]; + [speakButton setFrame:NSMakeRect(125, 70, 150, 40)]; + [[self.window contentView] addSubview:speakButton]; + + if (!SRAL_Initialize(0)) { + [self.engineLabel setStringValue:@"Engine: Failed to initialize SRAL!"]; + return; + } + + int engine = SRAL_GetCurrentEngine(); + const char *name = SRAL_GetEngineName(engine); + [self.engineLabel setStringValue: + [NSString stringWithFormat:@"Engine: %s", name ? name : "Unknown"]]; + + [self.window makeKeyAndOrderFront:nil]; + [NSApp activateIgnoringOtherApps:YES]; +} + +- (void)speakClicked:(id)sender { + SRAL_Speak("Hello, this is a test of the SRAL library.", true); + [self.speakingLabel setStringValue:@"Speaking: Yes"]; + + // Poll speaking status to update the label + [self.speakingTimer invalidate]; + self.speakingTimer = [NSTimer scheduledTimerWithTimeInterval:0.1 + target:self + selector:@selector(updateSpeakingStatus:) + userInfo:nil + repeats:YES]; +} + +- (void)updateSpeakingStatus:(NSTimer *)timer { + if (!SRAL_IsSpeaking()) { + [self.speakingLabel setStringValue:@"Speaking: No"]; + [timer invalidate]; + self.speakingTimer = nil; + } +} + +- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender { + return YES; +} + +- (void)applicationWillTerminate:(NSNotification *)notification { + [self.speakingTimer invalidate]; + SRAL_Uninitialize(); +} + +@end + +int main(int argc, const char *argv[]) { + @autoreleasepool { + NSApplication *app = [NSApplication sharedApplication]; + [app setActivationPolicy:NSApplicationActivationPolicyRegular]; + + AppDelegate *delegate = [[AppDelegate alloc] init]; + [app setDelegate:delegate]; + + [app run]; + } + return 0; +} diff --git a/Include/SRAL.h b/Include/SRAL.h index cb3a0ca..928ce8d 100644 --- a/Include/SRAL.h +++ b/Include/SRAL.h @@ -76,14 +76,14 @@ SRAL_ENGINE_SPEECH_DISPATCHER = 1 << 7, // --- Apple Screen Readers (macOS, iOS, etc.) --- -SRAL_ENGINE_NS_SPEECH = 1 << 8, - - /** @brief Apple VoiceOver, the built-in screen reader on macOS, iOS, and other Apple platforms. */ -SRAL_ENGINE_VOICE_OVER = 1 << 9, +SRAL_ENGINE_VOICE_OVER = 1 << 8, // --- Apple Speech Synthesis Engines (macOS, iOS, etc.) --- + +SRAL_ENGINE_NS_SPEECH = 1 << 9, + /** @brief AVFoundation Speech Synthesizer (AVSpeechSynthesizer), for text-to-speech on Apple platforms. */ SRAL_ENGINE_AV_SPEECH = 1 << 10 }; diff --git a/SRC/VoiceOver.mm b/SRC/VoiceOver.mm index b10971a..c360767 100644 --- a/SRC/VoiceOver.mm +++ b/SRC/VoiceOver.mm @@ -42,6 +42,8 @@ #if TARGET_OS_IOS || TARGET_OS_TV return UIAccessibilityIsVoiceOverRunning() == YES ? true : false; #elif TARGET_OS_OSX + // VoiceOver depends on a running NSApp, so return false if none is running + if (NSApp == nil) return false; return [[NSWorkspace sharedWorkspace] isVoiceOverEnabled]; #endif } From df905f2df6218300533bb7b74408439cf1f5bed9 Mon Sep 17 00:00:00 2001 From: Jesse Jurman Date: Mon, 16 Mar 2026 03:05:32 -0400 Subject: [PATCH 2/2] live update of engine and is_speaking --- Examples/ObjC/SRALCocoaExample.m | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/Examples/ObjC/SRALCocoaExample.m b/Examples/ObjC/SRALCocoaExample.m index d41c1b5..c9e3d3c 100644 --- a/Examples/ObjC/SRALCocoaExample.m +++ b/Examples/ObjC/SRALCocoaExample.m @@ -47,29 +47,29 @@ - (void)applicationDidFinishLaunching:(NSNotification *)notification { [self.engineLabel setStringValue: [NSString stringWithFormat:@"Engine: %s", name ? name : "Unknown"]]; + // Continuously poll engine and speaking status + self.speakingTimer = [NSTimer scheduledTimerWithTimeInterval:0.1 + target:self + selector:@selector(updateStatus:) + userInfo:nil + repeats:YES]; + [self.window makeKeyAndOrderFront:nil]; [NSApp activateIgnoringOtherApps:YES]; } - (void)speakClicked:(id)sender { SRAL_Speak("Hello, this is a test of the SRAL library.", true); - [self.speakingLabel setStringValue:@"Speaking: Yes"]; - - // Poll speaking status to update the label - [self.speakingTimer invalidate]; - self.speakingTimer = [NSTimer scheduledTimerWithTimeInterval:0.1 - target:self - selector:@selector(updateSpeakingStatus:) - userInfo:nil - repeats:YES]; } -- (void)updateSpeakingStatus:(NSTimer *)timer { - if (!SRAL_IsSpeaking()) { - [self.speakingLabel setStringValue:@"Speaking: No"]; - [timer invalidate]; - self.speakingTimer = nil; - } +- (void)updateStatus:(NSTimer *)timer { + int engine = SRAL_GetCurrentEngine(); + const char *name = SRAL_GetEngineName(engine); + [self.engineLabel setStringValue: + [NSString stringWithFormat:@"Engine: %s", name ? name : "None"]]; + + [self.speakingLabel setStringValue: + SRAL_IsSpeaking() ? @"Speaking: Yes" : @"Speaking: No"]; } - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender {