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..c9e3d3c --- /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"]]; + + // 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); +} + +- (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 { + 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 }