diff --git a/.gitignore b/.gitignore index 82cbca9..c0ba6cf 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,4 @@ build/ Release/ *.pyc *.pyd +.*DS_Store diff --git a/Bindings/go/SRAL/cgo_flags.go b/Bindings/go/SRAL/cgo_flags.go new file mode 100644 index 0000000..2e8c13e --- /dev/null +++ b/Bindings/go/SRAL/cgo_flags.go @@ -0,0 +1,11 @@ +package SRAL + +/* +#cgo CFLAGS: -I${SRCDIR}/../../../Include +#cgo darwin LDFLAGS: -L${SRCDIR}/lib -lSRAL -framework AppKit -framework Foundation -framework AVFoundation -lc++ +#cgo linux pkg-config: speech-dispatcher +#cgo linux LDFLAGS: -L${SRCDIR}/lib -lSRAL -lbrlapi -lstdc++ +#cgo windows CFLAGS: -DSRAL_STATIC +#cgo windows LDFLAGS: -L${SRCDIR}/lib -lSRAL -Wl,--start-group -luiautomationcore -lole32 -loleaut32 -luuid -luser32 -lkernel32 -lgdi32 -ladvapi32 -lshell32 -lstdc++ -Wl,--end-group -static +*/ +import "C" diff --git a/Bindings/go/SRAL/internal.go b/Bindings/go/SRAL/internal.go new file mode 100644 index 0000000..beed485 --- /dev/null +++ b/Bindings/go/SRAL/internal.go @@ -0,0 +1,174 @@ +package SRAL + +/* +#include +#include +#include +#include "SRAL.h" +*/ +import "C" + +import ( + "fmt" + "unsafe" +) + +func speak(engine Engine, text string, interrupt bool) bool { + cText := C.CString(text) + defer C.free(unsafe.Pointer(cText)) + if engine == NoSpecifiedEngine { + return bool(C.SRAL_Speak(cText, C.bool(interrupt))) + } + return bool(C.SRAL_SpeakEx(C.int(engine), cText, C.bool(interrupt))) +} + +func speakToMemory(engine Engine, text string) (*PCMData, error) { + cText := C.CString(text) + defer C.free(unsafe.Pointer(cText)) + var ( + bufSize C.uint64_t + channels C.int + sampleRate C.int + bits C.int + ) + + var ptr unsafe.Pointer + if engine == NoSpecifiedEngine { + ptr = C.SRAL_SpeakToMemory(cText, &bufSize, &channels, &sampleRate, &bits) + } else { + ptr = C.SRAL_SpeakToMemoryEx(C.int(engine), cText, &bufSize, &channels, &sampleRate, &bits) + } + if ptr == nil { + return nil, fmt.Errorf("failed to synthesize speech to memory") + } + defer C.SRAL_free(ptr) + data := C.GoBytes(ptr, C.int(bufSize)) + return &PCMData{ + Buffer: data, + Channels: int(channels), + SampleRate: int(sampleRate), + BitsPerSample: int(bits), + }, nil +} + +func speakSsml(engine Engine, ssml string, interrupt bool) bool { + cText := C.CString(ssml) + defer C.free(unsafe.Pointer(cText)) + if engine == NoSpecifiedEngine { + return bool(C.SRAL_SpeakSsml(cText, C.bool(interrupt))) + } + return bool(C.SRAL_SpeakSsmlEx(C.int(engine), cText, C.bool(interrupt))) +} + +func braille(engine Engine, text string) bool { + cText := C.CString(text) + defer C.free(unsafe.Pointer(cText)) + if engine == NoSpecifiedEngine { + return bool(C.SRAL_Braille(cText)) + } + return bool(C.SRAL_BrailleEx(C.int(engine), cText)) +} + +func output(engine Engine, text string, interrupt bool) bool { + cText := C.CString(text) + defer C.free(unsafe.Pointer(cText)) + if engine == NoSpecifiedEngine { + return bool(C.SRAL_Output(cText, C.bool(interrupt))) + } + return bool(C.SRAL_OutputEx(C.int(engine), cText, C.bool(interrupt))) +} + +func stopSpeech(engine Engine) bool { + if engine == NoSpecifiedEngine { + return bool(C.SRAL_StopSpeech()) + } + return bool(C.SRAL_StopSpeechEx(C.int(engine))) +} + +func pauseSpeech(engine Engine) bool { + if engine == NoSpecifiedEngine { + return bool(C.SRAL_PauseSpeech()) + } + return bool(C.SRAL_PauseSpeechEx(C.int(engine))) +} + +func resumeSpeech(engine Engine) bool { + if engine == NoSpecifiedEngine { + return bool(C.SRAL_ResumeSpeech()) + } + return bool(C.SRAL_ResumeSpeechEx(C.int(engine))) +} + +func isSpeaking(engine Engine) bool { + if engine == NoSpecifiedEngine { + return bool(C.SRAL_IsSpeaking()) + } + return bool(C.SRAL_IsSpeakingEx(C.int(engine))) +} + +func getIntParam(e Engine, p EngineParam, out *int) bool { + var cVal C.int + ptr := unsafe.Pointer(&cVal) + if !bool(C.SRAL_GetEngineParameter(C.int(e), C.int(p), ptr)) { + return false + } + *out = int(cVal) + return true +} + +func getBoolParam(e Engine, p EngineParam, out *bool) bool { + var val int = 0 + ptr := unsafe.Pointer(&val) + if !bool(C.SRAL_GetEngineParameter(C.int(e), C.int(p), ptr)) { + return false + } + if val == 0 { + *out = false + } else { + *out = true + } + return true +} + +func getVoicesParam(e Engine, p EngineParam, out *[]VoiceInfo) bool { + var count int + if !getIntParam(e, VoiceCountParam, &count) { + return false + } + size := C.size_t(count) * C.size_t(unsafe.Sizeof(C.SRAL_VoiceInfo{})) + ptr := C.SRAL_malloc(size) + if ptr == nil { + return false + } + defer C.SRAL_free(ptr) + if !bool(C.SRAL_GetEngineParameter(C.int(e), C.int(p), ptr)) { + return false + } + + cVoices := unsafe.Slice((*C.SRAL_VoiceInfo)(ptr), count) + res := make([]VoiceInfo, count) + for i := range cVoices { + res[i] = VoiceInfo{ + Index: int(cVoices[i].index), + Name: C.GoString(cVoices[i].name), + Language: C.GoString(cVoices[i].language), + Gender: C.GoString(cVoices[i].gender), + Vendor: C.GoString(cVoices[i].vendor), + } + } + *out = res + return true +} + +func setIntParam(e Engine, p EngineParam, val int) bool { + cVal := C.int(val) + return bool(C.SRAL_SetEngineParameter(C.int(e), C.int(p), unsafe.Pointer(&cVal))) +} + +func setBoolParam(e Engine, p EngineParam, val bool) bool { + var valAsNum int = 0 + if val { + valAsNum = 1 + } + return setIntParam(e, p, valAsNum) +} diff --git a/Bindings/go/SRAL/sral.go b/Bindings/go/SRAL/sral.go new file mode 100644 index 0000000..6e07e3e --- /dev/null +++ b/Bindings/go/SRAL/sral.go @@ -0,0 +1,225 @@ +// Package SRAL (Screen Reader Abstraction Library) provides a unified interface +// for interacting with various speech engines. +// It abstracts the differences between multiple speech engines, allowing developers to +// implement accessibility features in their applications without needing to handle the +// specifics of each engine. +package SRAL + +/* +#include "SRAL.h" +*/ +import "C" + +import ( + "time" +) + +// Speak speaks the given text using the current engine. +// The interrupt flag indicates whether to interrupt the current speech. +// Returns true if speaking was successful, false otherwise. +func Speak(text string, interrupt bool) bool { + return speak(NoSpecifiedEngine, text, interrupt) +} + +// SpeakToMemory speaks the given text into memory using the current engine. +// Returns a PCMData pointer containing the buffer and its properties if successful. +func SpeakToMemory(text string) (*PCMData, error) { + return speakToMemory(NoSpecifiedEngine, text) +} + +// SpeakSsml speaks the given valid SSML string using the current engine. +// The interrupt flag indicates whether to interrupt the current speech. +// Returns true if speaking was successful, false otherwise. +func SpeakSsml(ssml string, interrupt bool) bool { + return speakSsml(NoSpecifiedEngine, ssml, interrupt) +} + +// Braille outputs the given text to a Braille display using the current engine. +// Returns true if Braille output was successful, false otherwise. +func Braille(text string) bool { + return braille(NoSpecifiedEngine, text) +} + +// Output outputs text using all currently supported speech engine methods. +// The interrupt flag indicates whether to interrupt speech. +// Returns true if output was successful, false otherwise. +func Output(text string, interrupt bool) bool { + return output(NoSpecifiedEngine, text, interrupt) +} + +// StopSpeech stops speech if it is active on the current engine. +// Returns true if speech was stopped successfully, false otherwise. +func StopSpeech() bool { + return stopSpeech(NoSpecifiedEngine) +} + +// PauseSpeech pauses speech if it is active and the current speech engine supports this. +// Returns true if speech was paused successfully, false otherwise. +func PauseSpeech() bool { + return pauseSpeech(NoSpecifiedEngine) +} + +// ResumeSpeech resumes speech if it was paused and the current speech engine supports this. +// Returns true if speech was resumed successfully, false otherwise. +func ResumeSpeech() bool { + return resumeSpeech(NoSpecifiedEngine) +} + +// IsSpeaking returns true if the current engine is currently speaking, false otherwise. +func IsSpeaking() bool { + return isSpeaking(NoSpecifiedEngine) +} + +// GetCurrentEngine returns the identifier of the current speech engine in use. +func GetCurrentEngine() Engine { + return Engine(C.SRAL_GetCurrentEngine()) +} + +// GetEngineFeatures returns the features supported by the specified engine. +func GetEngineFeatures(engine Engine) Feature { + return Feature(C.SRAL_GetEngineFeatures(C.int(engine))) +} + +// GetEngineParameter retrieves a parameter for the specified speech engine. +// The out parameter must be a pointer to int, bool, or a slice of VoiceInfo. +// Returns true if the parameter was retrieved successfully. +func GetEngineParameter(engine Engine, param EngineParam, out any) bool { + switch v := out.(type) { + case *int: + return getIntParam(engine, param, v) + case *bool: + return getBoolParam(engine, param, v) + case *[]VoiceInfo: + return getVoicesParam(engine, param, v) + default: + return false + } +} + +// SetEngineParameter sets a parameter for the specified speech engine. +// The value can be an int or a bool. +// Returns true if the parameter was set successfully. +func SetEngineParameter(engine Engine, param EngineParam, value any) bool { + switch v := value.(type) { + case int: + return setIntParam(engine, param, v) + case bool: + return setBoolParam(engine, param, v) + default: + return false + } +} + +// Initialize initializes the library and optionally excludes certain engines +// from auto-update via a bitmask. Returns true if successful. +func Initialize(excluded_engines Engine) bool { + return bool(C.SRAL_Initialize(C.int(excluded_engines))) +} + +// Uninitialize uninitializes the library, freeing resources. +func Uninitialize() { + C.SRAL_Uninitialize() +} + +// IsInitialized checks if the library has been initialized. +func IsInitialized() bool { + return bool(C.SRAL_IsInitialized()) +} + +// SpeakEx speaks the given text with the specified engine. +// Returns true if speaking was successful. +func SpeakEx(engine Engine, text string, interrupt bool) bool { + return speak(engine, text, interrupt) +} + +// SpeakToMemoryEx speaks the given text into memory with the specified engine. +func SpeakToMemoryEx(engine Engine, text string) (*PCMData, error) { + return speakToMemory(engine, text) +} + +// SpeakSsmlEx speaks the given SSML string with the specified engine. +func SpeakSsmlEx(engine Engine, ssml string, interrupt bool) bool { + return speakSsml(engine, ssml, interrupt) +} + +// BrailleEx outputs text to a Braille display using the specified engine. +func BrailleEx(engine Engine, text string) bool { + return braille(engine, text) +} + +// OutputEx outputs text using the specified engine. +func OutputEx(engine Engine, text string, interrupt bool) bool { + return output(engine, text, interrupt) +} + +// StopSpeechEx stops speech for the specified engine. +func StopSpeechEx(engine Engine) bool { + return stopSpeech(engine) +} + +// PauseSpeechEx pauses speech for the specified engine. +func PauseSpeechEx(engine Engine) bool { + return pauseSpeech(engine) +} + +// ResumeSpeechEx resumes speech for the specified engine. +func ResumeSpeechEx(engine Engine) bool { + return resumeSpeech(engine) +} + +// IsSpeakingEx returns true if the specified engine is currently speaking. +func IsSpeakingEx(engine Engine) bool { + return isSpeaking(engine) +} + +// Delay delays the next speech or output operation by the given duration. +func Delay(t time.Duration) { + cMs := C.int(t.Milliseconds()) + C.SRAL_Delay(cMs) +} + +// RegisterKeyboardHooks installs global keyboard hooks for speech interruption (Ctrl) +// and pause (Shift) for engines like SAPI or SpeechDispatcher. +// Returns true if hooks were successfully installed. +func RegisterKeyboardHooks() bool { + return bool(C.SRAL_RegisterKeyboardHooks()) +} + +// UnregisterKeyboardHooks uninstalls the speech interruption and pause keyboard hooks. +func UnregisterKeyboardHooks() { + C.SRAL_UnregisterKeyboardHooks() +} + +// GetAvailableEngines returns a bitmask of all available engines for the current platform. +func GetAvailableEngines() Engine { + engines := C.SRAL_GetAvailableEngines() + return Engine(engines) +} + +// GetActiveEngines returns a bitmask of all active engines that can be used. +func GetActiveEngines() Engine { + engines := C.SRAL_GetActiveEngines() + return Engine(engines) +} + +// GetEngineName returns the name of the specified engine. +func GetEngineName(engine Engine) string { + cName := C.SRAL_GetEngineName(C.int(engine)) + name := C.GoString(cName) + return name +} + +// SetEnginesExclude sets a bitmask of engines to be excluded from auto-update. +func SetEnginesExclude(excluded_engines Engine) bool { + return bool(C.SRAL_SetEnginesExclude(C.int(excluded_engines))) +} + +// GetEnginesExclude returns the bitmask of engines excluded from auto-update. +// Returns InvalidEngine if the library is not initialized. +func GetEnginesExclude() Engine { + excluded_engines := int(C.SRAL_GetEnginesExclude()) + if excluded_engines == -1 { + return InvalidEngine + } + return Engine(excluded_engines) +} diff --git a/Bindings/go/SRAL/types.go b/Bindings/go/SRAL/types.go new file mode 100644 index 0000000..f01c5fd --- /dev/null +++ b/Bindings/go/SRAL/types.go @@ -0,0 +1,112 @@ +package SRAL + +// Engine represents bit flags identifying various accessibility engines, +// such as screen readers, speech synthesis engines, or accessibility frameworks. +type Engine int + +const ( + // NoneEngine indicates no specific engine identified, or engine is unknown. + NoneEngine Engine = 0 + // NVDAEngine — NonVisual Desktop Access (NVDA) for Windows. + NVDAEngine Engine = Engine(1 << iota) + // JAWSEngine — Job Access With Speech (JAWS) for Windows. + JAWSEngine + // ZDSREngine — Zhengdu Screen Reader (ZDSR) for Windows. + ZDSREngine + // NarratorEngine — Microsoft Narrator, the built-in screen reader for Windows. + NarratorEngine + // UIAEngine — Microsoft UI Automation (UIA) framework for Windows. + UIAEngine + // SAPIEngine — Microsoft Speech API (SAPI) for text-to-speech on Windows. + 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 + // AVSpeechEngine — AVFoundation Speech Synthesizer (AVSpeechSynthesizer) for Apple platforms. + AVSpeechEngine + // AllEngines is a bitmask of all supported engines. + AllEngines Engine = NVDAEngine | JAWSEngine | ZDSREngine | NarratorEngine | UIAEngine | SAPIEngine | SpeechDispatcherEngine | NSSpeechEngine | VoiceOverEngine | AVSpeechEngine + // InvalidEngine represents an error or uninitialized engine state. + InvalidEngine Engine = -1 + // NoSpecifiedEngine is used for auto-selection of the engine. + NoSpecifiedEngine Engine = -255 +) + +// Feature defines the features supported by various speech engines. +type Feature int + +const ( + // NoneFeature indicates no features supported. + NoneFeature Feature = 0 + // SpeechFeature indicates support for text-to-speech. + SpeechFeature = Feature(1 << iota) + // BrailleFeature indicates support for Braille output. + BrailleFeature + // SpeechRateFeature indicates support for adjusting speech rate. + SpeechRateFeature + // SpeechVolumeFeature indicates support for adjusting speech volume. + SpeechVolumeFeature + // SelectVoiceFeature indicates support for selecting different voices. + SelectVoiceFeature + // PauseSpeechFeature indicates support for pausing speech. + PauseSpeechFeature + // SSMLFeature indicates support for SSML tags. + SSMLFeature + // SpeakToMemoryFeature indicates support for synthesizing speech to a PCM buffer. + SpeakToMemoryFeature + // SpellingFeature indicates support for spelling out text. + SpellingFeature + // AllFeatures is a bitmask of all available engine features. + AllFeatures = SpeechFeature | BrailleFeature | SpeechRateFeature | SpeechVolumeFeature | SelectVoiceFeature | PauseSpeechFeature | SSMLFeature | SpeakToMemoryFeature | SpellingFeature +) + +// IsSupported checks if the feature set includes the specified feature. +func (f Feature) IsSupported(other Feature) bool { + return (f & other) != 0 +} + +// EngineParam defines parameters that can be set or retrieved from a speech engine. +type EngineParam int + +const ( + // SpeechRateParam — parameter for speech rate. + SpeechRateParam EngineParam = EngineParam(iota) + // SpeechVolumeParam — parameter for speech volume. + SpeechVolumeParam + // VoiceIndexParam — index of the current voice. + VoiceIndexParam + // VoicePropertiesParam — properties of the available voices. + VoicePropertiesParam + // VoiceCountParam — total number of available voices. + VoiceCountParam + // SymbolLevelParam — punctuation/symbol speaking level. + SymbolLevelParam + // SAPITrimThresholdParam — trim threshold specifically for SAPI. + SAPITrimThresholdParam + // EnableSpellingParam — toggles spelling mode. + EnableSpellingParam + // UseCharacterDescriptionsParam — toggles character descriptions. + UseCharacterDescriptionsParam + // NVDAIsControlExParam — specific parameter for NVDA control. + NVDAIsControlExParam +) + +// VoiceInfo contains information about a specific synthesizer voice. +type VoiceInfo struct { + Index int // Index of the voice within the engine + Name string // Name of the voice + Language string // Language code (e.g., "en-US") + Gender string // Gender of the voice + Vendor string // Vendor or developer of the voice +} + +// PCMData represents raw audio data (PCM) generated when speaking to memory. +type PCMData struct { + Buffer []byte // Raw audio buffer + Channels int // Number of audio channels + SampleRate int // Sample rate in Hz + BitsPerSample int // Bit depth (bits per sample) +} diff --git a/Bindings/go/SRAL_example/main.go b/Bindings/go/SRAL_example/main.go new file mode 100644 index 0000000..8bfecbb --- /dev/null +++ b/Bindings/go/SRAL_example/main.go @@ -0,0 +1,643 @@ +package main + +import ( + "SRAL" + "bufio" + "fmt" + "math" + "os" + "time" +) + +func promptUser(message string) { + fmt.Printf("\n>>> %s (Press Enter to continue)...", message) + reader := bufio.NewReader(os.Stdin) + _, err := reader.ReadBytes('\n') + if err != nil { + if err.Error() == "EOF" { + fmt.Println("EOF detected on stdin, continuing without prompt.") + } + } +} + +func PrintEngineNames(engineBitmask SRAL.Engine, title string) { + fmt.Printf("%s:\n", title) + if engineBitmask == SRAL.NoneEngine { + fmt.Printf(" (None)\n") + return + } + found := false + var engine_val SRAL.Engine + for engine_val = SRAL.NVDAEngine; engine_val <= SRAL.AVSpeechEngine; engine_val <<= 1 { + if engineBitmask&engine_val != 0 { + name := SRAL.GetEngineName(engine_val) + fmt.Printf(" - %s (%d)\n", name, int(engine_val)) + found = true + } + } + if !found && engineBitmask != SRAL.NoneEngine { + fmt.Printf(" (Unknown bitmask: %d)\n", int(engineBitmask)) + } + fmt.Printf("\n") +} + +func printSupportedFeatures(features SRAL.Feature) { + fmt.Printf("Supported Features (%d):\n", int(features)) + if features == SRAL.NoneFeature { + fmt.Printf(" (None)\n") + return + } + if features.IsSupported(SRAL.SpeechFeature) { + fmt.Printf(" - SUPPORTS_SPEECH\n") + } + if features.IsSupported(SRAL.BrailleFeature) { + fmt.Printf(" - SUPPORTS_BRAILLE\n") + } + if features.IsSupported(SRAL.SpeechRateFeature) { + fmt.Printf(" - SUPPORTS_SPEECH_RATE\n") + } + if features.IsSupported(SRAL.SpeechVolumeFeature) { + fmt.Printf(" - SUPPORTS_SPEECH_VOLUME\n") + } + if features.IsSupported(SRAL.SelectVoiceFeature) { + fmt.Printf(" - SUPPORTS_SELECT_VOICE\n") + } + if features.IsSupported(SRAL.PauseSpeechFeature) { + fmt.Printf(" - SUPPORTS_PAUSE_SPEECH\n") + } + if features.IsSupported(SRAL.SSMLFeature) { + fmt.Printf(" - SUPPORTS_SSML\n") + } + if features.IsSupported(SRAL.SpeakToMemoryFeature) { + fmt.Printf(" - SUPPORTS_SPEAK_TO_MEMORY\n") + } + if features.IsSupported(SRAL.SpellingFeature) { + fmt.Printf(" - SUPPORTS_SPELLING\n") + } + fmt.Printf("\n") +} + +func TestSection(name string) { + fmt.Printf("\n\n========================================\n") + fmt.Printf(" Testing: %s\n", name) + fmt.Printf("========================================\n") +} + +func Check(condition bool, success_msg, fail_msg string) { + if condition { + fmt.Printf("[SUCCESS] %s\n", success_msg) + } else { + fmt.Printf("[FAILURE] %s\n", fail_msg) + } +} + +func CheckSRAL(condition bool, action_desc string) { + if condition { + fmt.Printf("[SUCCESS] %s\n", action_desc) + } else { + fmt.Printf("[FAILURE] %s\n", action_desc) + } +} + +func flagStatus(fl bool) string { + if fl { + return "Enabled" + } + return "Disabled" +} + +func main() { + fmt.Printf("SRAL Tester\n") + fmt.Printf("-------------------------\n") + TestSection("SRAL_IsInitialized (Before Initialization)") + Check(!SRAL.IsInitialized(), "SRAL_IsInitialized correctly returns false before init.", "SRAL_IsInitialized returned true before init!") + TestSection("SRAL_Initialize") + engines_to_exclude := SRAL.NoneEngine + engines_to_exclude = SRAL.UIAEngine + fmt.Printf("Attempting to initialize SRAL, excluding engines: %d (%s)\n", + engines_to_exclude, SRAL.GetEngineName(engines_to_exclude)) + + if SRAL.Initialize(engines_to_exclude) { + fmt.Printf("[SUCCESS] SRAL_Initialize successful.\n") + } else { + fmt.Printf("[FAILURE] SRAL_Initialize failed. Some tests may not run or behave as expected. Exiting.\n") + return + } + Check(SRAL.IsInitialized(), "SRAL_IsInitialized correctly returns true after init.", "SRAL_IsInitialized returned false after init!") + TestSection("Engine Information") + available_engines := SRAL.GetAvailableEngines() + PrintEngineNames(available_engines, "Available Engines on this Platform") + + active_engines := SRAL.GetActiveEngines() + PrintEngineNames(active_engines, "Currently Active/Usable Engines") + + current_engine_id := SRAL.GetCurrentEngine() + fmt.Printf("Current Default Engine: %s (%d)\n", SRAL.GetEngineName(current_engine_id), current_engine_id) + + fmt.Printf("\nNames of all SRAL_Engines enum members (as per SRAL_GetEngineName):\n") + for e_val := SRAL.NVDAEngine; e_val <= SRAL.AVSpeechEngine; e_val <<= 1 { + name := SRAL.GetEngineName(SRAL.Engine(e_val)) + fmt.Printf(" Engine ID %d: %s\n", e_val, name) + } + + specific_engine_for_ex_tests := SRAL.NoneEngine + if active_engines != SRAL.NoneEngine { + for e_val := SRAL.NVDAEngine; e_val <= SRAL.AVSpeechEngine; e_val <<= 1 { + e := SRAL.Engine(e_val) + if ((active_engines & e) != 0) && e != current_engine_id { + specific_engine_for_ex_tests = e + break + } + } + if specific_engine_for_ex_tests == SRAL.NoneEngine { + for e_val := SRAL.NVDAEngine; e_val <= SRAL.AVSpeechEngine; e_val <<= 1 { + e := SRAL.Engine(e_val) + if (active_engines & e) != 0 { + specific_engine_for_ex_tests = e + break + } + } + } + } + + if specific_engine_for_ex_tests != SRAL.NoneEngine { + fmt.Printf("\nWill use engine '%s' (%d) for specific engine (Ex) tests.\n", + SRAL.GetEngineName(specific_engine_for_ex_tests), specific_engine_for_ex_tests) + } else { + fmt.Printf("\nNo specific engine distinct from default (or no active engines) for Ex tests. Ex tests might target default or fail if engine doesn't support action.\n") + } + + TestSection("Keyboard Hooks") + + if SRAL.RegisterKeyboardHooks() { + fmt.Printf("[SUCCESS] SRAL_RegisterKeyboardHooks registered.\n") + promptUser("Keyboard hooks (Ctrl=Interrupt, Shift=Pause/Resume) are active. Test them with upcoming speech. This primarily affects non-screen-reader engines like SAPI or SpeechDispatcher.") + } else { + fmt.Printf("[INFO] SRAL_RegisterKeyboardHooks failed or did not register. This might be expected if no suitable engine is active or if permissions are lacking.\n") + } + + TestSection("SRAL_GetEngineFeatures") + fmt.Printf("Features for Current Default Engine (%s):\n", SRAL.GetEngineName(current_engine_id)) + current_engine_features := SRAL.GetEngineFeatures(SRAL.NoneEngine) + printSupportedFeatures(current_engine_features) + + if specific_engine_for_ex_tests != SRAL.NoneEngine { + fmt.Printf("Features for Specific Engine selected for Ex tests (%s):\n", SRAL.GetEngineName(specific_engine_for_ex_tests)) + specific_engine_features := SRAL.GetEngineFeatures(specific_engine_for_ex_tests) + printSupportedFeatures(specific_engine_features) + } + + if current_engine_features.IsSupported(SRAL.SpeechFeature) { + TestSection("SRAL_Speak (Default Engine)") + CheckSRAL(SRAL.Speak("Testing SRAL Speak, not interrupting previous speech.", false), "SRAL_Speak (no interrupt)") + time.Sleep(2000 * time.Millisecond) + CheckSRAL(SRAL.Speak("Testing SRAL Speak, interrupting previous speech.", true), "SRAL_Speak (interrupt)") + time.Sleep(2000 * time.Millisecond) + + if specific_engine_for_ex_tests != SRAL.NoneEngine { + TestSection("SRAL_SpeakEx (Specific Engine)") + features_ex := SRAL.GetEngineFeatures(specific_engine_for_ex_tests) + if features_ex.IsSupported(SRAL.SpeechFeature) { + CheckSRAL(SRAL.SpeakEx(specific_engine_for_ex_tests, "Testing SRAL SpeakEx, not interrupting.", false), "SRAL_SpeakEx (no interrupt)") + time.Sleep(2000 * time.Millisecond) + CheckSRAL(SRAL.SpeakEx(specific_engine_for_ex_tests, "Testing SRAL SpeakEx, interrupting.", true), "SRAL_SpeakEx (interrupt)") + time.Sleep(2000 * time.Millisecond) + } else { + fmt.Printf("Specific engine %s does not support speech for SpeakEx.\n", SRAL.GetEngineName(specific_engine_for_ex_tests)) + } + } + } else { + fmt.Printf("Current default engine does not support speech. Skipping speech tests.\n") + } + + if current_engine_features.IsSupported(SRAL.SSMLFeature) { + TestSection("SRAL_SpeakSsml (Default Engine)") + ssml_test := "This is SSML text." + CheckSRAL(SRAL.SpeakSsml(ssml_test, true), "SRAL_SpeakSsml") + time.Sleep(3000 * time.Millisecond) + + if specific_engine_for_ex_tests != SRAL.NoneEngine { + TestSection("SRAL_SpeakSsmlEx (Specific Engine)") + features_ex := SRAL.GetEngineFeatures(specific_engine_for_ex_tests) + if features_ex.IsSupported(SRAL.SSMLFeature) { + CheckSRAL(SRAL.SpeakSsmlEx(specific_engine_for_ex_tests, ssml_test, true), "SRAL_SpeakSsmlEx") + time.Sleep(3000 * time.Millisecond) + } else { + fmt.Printf("Specific engine %s does not support SSML for SpeakSsmlEx.\n", SRAL.GetEngineName(specific_engine_for_ex_tests)) + } + } + } else { + fmt.Printf("Current default engine does not support SSML. Skipping SSML tests.\n") + } + + if current_engine_features.IsSupported(SRAL.SpeakToMemoryFeature) { + TestSection("SRAL_SpeakToMemory (Default Engine)") + pcm_data, err := SRAL.SpeakToMemory("Testing speak to memory audio synthesis.") + if err == nil { + fmt.Printf("[SUCCESS] SRAL_SpeakToMemory successful.\n") + fmt.Printf(" Buffer Size: %d bytes\n", len(pcm_data.Buffer)) + fmt.Printf(" Channels: %d\n", pcm_data.Channels) + fmt.Printf(" Sample Rate: %d Hz\n", pcm_data.SampleRate) + fmt.Printf(" Bits Per Sample: %d\n", pcm_data.BitsPerSample) + fmt.Printf(" No release required for PCM buffer.\n") + } else { + fmt.Printf("[FAILURE] SRAL_SpeakToMemory failed.\n") + } + + if specific_engine_for_ex_tests != SRAL.NoneEngine { + TestSection("SRAL_SpeakToMemoryEx (Specific Engine)") + features_ex := SRAL.GetEngineFeatures(specific_engine_for_ex_tests) + if features_ex.IsSupported(SRAL.SpeakToMemoryFeature) { + pcm_data, err = SRAL.SpeakToMemoryEx(specific_engine_for_ex_tests, "Testing speak to memory ex.") + if err == nil { + fmt.Printf("[SUCCESS] SRAL_SpeakToMemoryEx successful for engine %s.\n", SRAL.GetEngineName(specific_engine_for_ex_tests)) + fmt.Printf(" Buffer Size: %d bytes\n", len(pcm_data.Buffer)) + fmt.Printf(" Channels: %d\n", pcm_data.Channels) + fmt.Printf(" Sample Rate: %d Hz\n", pcm_data.SampleRate) + fmt.Printf(" Bits Per Sample: %d\n", pcm_data.BitsPerSample) + fmt.Printf(" No release required for PCM buffer.\n") + } else { + fmt.Printf("[FAILURE] SRAL_SpeakToMemoryEx failed for engine %s.\n", SRAL.GetEngineName(specific_engine_for_ex_tests)) + } + } else { + fmt.Printf("Specific engine %s does not support Speak To Memory for SpeakToMemoryEx.\n", SRAL.GetEngineName(specific_engine_for_ex_tests)) + } + } + } else { + fmt.Printf("Current default engine does not support Speak To Memory. Skipping these tests.\n") + } + + if current_engine_features.IsSupported(SRAL.BrailleFeature) { + TestSection("SRAL_Braille (Default Engine)") + promptUser("Prepare to check Braille display for 'Testing SRAL Braille output.'") + CheckSRAL(SRAL.Braille("Testing SRAL Braille output."), "SRAL_Braille") + + if specific_engine_for_ex_tests != SRAL.NoneEngine { + TestSection("SRAL_BrailleEx (Specific Engine)") + features_ex := SRAL.GetEngineFeatures(specific_engine_for_ex_tests) + if features_ex.IsSupported(SRAL.BrailleFeature) { + promptUser("Prepare to check Braille display for 'Testing SRAL Braille Ex output.'") + CheckSRAL(SRAL.BrailleEx(specific_engine_for_ex_tests, "Testing SRAL Braille Ex output."), "SRAL_BrailleEx") + } else { + fmt.Printf("Specific engine %s does not support Braille for BrailleEx.\n", SRAL.GetEngineName(specific_engine_for_ex_tests)) + } + } + } else { + fmt.Printf("Current default engine does not support Braille. Skipping Braille tests.\n") + } + + TestSection("SRAL_Output (Default Engine)") + promptUser("Prepare for SRAL_Output (Speech and/or Braille) for 'Testing SRAL Output, not interrupting.'") + CheckSRAL(SRAL.Output("Testing SRAL Output, not interrupting.", false), "SRAL_Output (no interrupt)") + time.Sleep(2000 * time.Millisecond) + promptUser("Prepare for SRAL_Output (Speech and/or Braille) for 'Testing SRAL Output, interrupting.'") + CheckSRAL(SRAL.Output("Testing SRAL Output, interrupting now.", true), "SRAL_Output (interrupt)") + time.Sleep(2000 * time.Millisecond) + + if specific_engine_for_ex_tests != SRAL.NoneEngine { + TestSection("SRAL_OutputEx (Specific Engine)") + promptUser("Prepare for SRAL_OutputEx with specific engine for 'Testing SRAL OutputEx, not interrupting.'") + CheckSRAL(SRAL.OutputEx(specific_engine_for_ex_tests, "Testing SRAL OutputEx, not interrupting.", false), "SRAL_OutputEx (no interrupt)") + time.Sleep(2000 * time.Millisecond) + promptUser("Prepare for SRAL_OutputEx with specific engine for 'Testing SRAL OutputEx, interrupting.'") + CheckSRAL(SRAL.OutputEx(specific_engine_for_ex_tests, "Testing SRAL OutputEx, interrupting now.", true), "SRAL_OutputEx (interrupt)") + time.Sleep(2000 * time.Millisecond) + } + + if current_engine_features.IsSupported(SRAL.SpeechFeature) { + TestSection("Speech Control (Default Engine)") + long_speech := "This is a moderately long sentence designed to test the pause, resume, and stop functionality of the SRAL library effectively." + fmt.Printf("Speaking long sentence with default engine: \"%s\"\n", long_speech) + SRAL.Speak(long_speech, true) + promptUser("Speech started. Press Enter to attempt PAUSE (if supported).") + status := "False" + if SRAL.IsSpeaking() { + status = "true" + } + fmt.Printf("IsSpeaking status: %s", status) + + if current_engine_features.IsSupported(SRAL.PauseSpeechFeature) { + CheckSRAL(SRAL.PauseSpeech(), "SRAL_PauseSpeech") + promptUser("Speech Paused (hopefully). Press Enter to attempt RESUME.") + CheckSRAL(SRAL.ResumeSpeech(), "SRAL_ResumeSpeech") + promptUser("Speech Resumed (hopefully). Press Enter to STOP.") + } else { + fmt.Printf("Pause/Resume not supported by current default engine according to features. Will attempt stop directly.\n") + promptUser("Speech should be ongoing. Press Enter to STOP.") + } + CheckSRAL(SRAL.StopSpeech(), "SRAL_StopSpeech") + fmt.Printf("Speech should be stopped now.\n") + time.Sleep(500 * time.Millisecond) + + if specific_engine_for_ex_tests != SRAL.NoneEngine { + TestSection("Speech Control Ex (Specific Engine)") + features_ex := SRAL.GetEngineFeatures(specific_engine_for_ex_tests) + if features_ex.IsSupported(SRAL.SpeechFeature) { + fmt.Printf("Speaking long sentence with engine %s: \"%s\"\n", SRAL.GetEngineName(specific_engine_for_ex_tests), long_speech) + SRAL.SpeakEx(specific_engine_for_ex_tests, long_speech, true) + promptUser("Speech started (Ex). Press Enter to PAUSE (Ex) (if supported).") + status := "False" + if SRAL.IsSpeakingEx(specific_engine_for_ex_tests) { + status = "true" + } + fmt.Printf("IsSpeaking status: %s", status) + + if features_ex.IsSupported(SRAL.PauseSpeechFeature) { + CheckSRAL(SRAL.PauseSpeechEx(specific_engine_for_ex_tests), "SRAL_PauseSpeechEx") + promptUser("Speech Paused (Ex). Press Enter to RESUME (Ex).") + CheckSRAL(SRAL.ResumeSpeechEx(specific_engine_for_ex_tests), "SRAL_ResumeSpeechEx") + promptUser("Speech Resumed (Ex). Press Enter to STOP (Ex).") + } else { + fmt.Printf("Pause/Resume not supported by specific engine %s. Will attempt stop directly.\n", SRAL.GetEngineName(specific_engine_for_ex_tests)) + promptUser("Speech should be ongoing (Ex). Press Enter to STOP (Ex).") + } + CheckSRAL(SRAL.StopSpeechEx(specific_engine_for_ex_tests), "SRAL_StopSpeechEx") + fmt.Printf("Speech should be stopped (Ex).\n") + time.Sleep(500 * time.Millisecond) + } else { + fmt.Printf("Specific engine %s does not support speech. Skipping Speech Control Ex tests.\n", SRAL.GetEngineName(specific_engine_for_ex_tests)) + } + } + } + + TestSection("SRAL Engine Parameters (Default Engine)") + + if current_engine_features.IsSupported(SRAL.SpeechRateFeature) { + fmt.Printf("\nTesting SPEECH_RATE (Default Engine):\n") + var original_rate int + if SRAL.GetEngineParameter(SRAL.NoneEngine, SRAL.SpeechRateParam, &original_rate) { + fmt.Printf(" Original rate: %d\n", original_rate) + new_rate := original_rate + 10 + if original_rate > 90 { + new_rate = original_rate - 10 + } + if new_rate < 0 { + new_rate = 0 + } + if new_rate > 100 { + new_rate = 100 + } + + fmt.Printf(" Attempting to set rate to: %d\n", new_rate) + if SRAL.SetEngineParameter(SRAL.NoneEngine, SRAL.SpeechRateParam, new_rate) { + var fetched_rate int + SRAL.GetEngineParameter(SRAL.NoneEngine, SRAL.SpeechRateParam, &fetched_rate) + fmt.Printf(" New rate confirmed by get: %d\n", fetched_rate) + Check(math.Abs(float64(fetched_rate-new_rate)) <= 5, "Rate set and get matches (or is close)", "Rate set/get mismatch or significant difference") + SRAL.Speak("Testing new speech rate setting.", true) + time.Sleep(2500 * time.Millisecond) + SRAL.SetEngineParameter(SRAL.NoneEngine, SRAL.SpeechRateParam, original_rate) + fmt.Printf(" Restored original rate to: %d\n", original_rate) + } else { + fmt.Printf(" Failed to set SPEECH_RATE.\n") + } + } else { + fmt.Printf(" Failed to get initial SPEECH_RATE.\n") + } + } else { + fmt.Printf("\nSPEECH_RATE not supported by current default engine.\n") + } + + if current_engine_features.IsSupported(SRAL.SpeechVolumeFeature) { + fmt.Printf("\nTesting SPEECH_VOLUME (Default Engine):\n") + var original_volume int + if SRAL.GetEngineParameter(SRAL.NoneEngine, SRAL.SpeechVolumeParam, &original_volume) { + fmt.Printf(" Original volume: %d\n", original_volume) + new_volume := original_volume + 10 + if original_volume > 90 { + new_volume = original_volume - 10 + } + if new_volume < 0 { + new_volume = 0 + } + if new_volume > 100 { + new_volume = 100 + } + + fmt.Printf(" Attempting to set volume to: %d\n", new_volume) + if SRAL.SetEngineParameter(SRAL.NoneEngine, SRAL.SpeechVolumeParam, new_volume) { + var fetched_volume int + SRAL.GetEngineParameter(SRAL.NoneEngine, SRAL.SpeechVolumeParam, &fetched_volume) + fmt.Printf(" New volume confirmed by get: %d\n", fetched_volume) + Check(math.Abs(float64(fetched_volume-new_volume)) <= 5, "Volume set and get matches (or is close)", "Volume set/get mismatch or significant difference") + SRAL.Speak("Testing new speech volume setting.", true) + time.Sleep(2500 * time.Millisecond) + SRAL.SetEngineParameter(SRAL.NoneEngine, SRAL.SpeechVolumeParam, original_volume) + fmt.Printf(" Restored original volume to: %d\n", original_volume) + } else { + fmt.Printf(" Failed to set SPEECH_VOLUME.\n") + } + } else { + fmt.Printf(" Failed to get initial SPEECH_VOLUME.\n") + } + } else { + fmt.Printf("\nSPEECH_VOLUME not supported by current default engine.\n") + } + + if current_engine_features.IsSupported(SRAL.SelectVoiceFeature) { + fmt.Printf("\nTesting VOICE parameters (Default Engine):\n") + voice_count := 0 + if SRAL.GetEngineParameter(SRAL.NoneEngine, SRAL.VoiceCountParam, &voice_count) { + fmt.Printf(" Voice count: %d\n", voice_count) + if voice_count > 0 { + voice_infos := []SRAL.VoiceInfo{} + if SRAL.GetEngineParameter(SRAL.NoneEngine, SRAL.VoicePropertiesParam, &voice_infos) { + fmt.Printf(" Available voices:\n") + for i := 0; i < voice_count; i++ { + fmt.Printf(" %d: %s\n", i, voice_infos[i].Name) + fmt.Printf(" %d: %s\n", i, voice_infos[i].Language) + fmt.Printf(" %d: %s\n", i, voice_infos[i].Gender) + fmt.Printf(" %d: %s\n", i, voice_infos[i].Vendor) + } + + current_voice_index := -1 + original_voice_index := -1 + SRAL.GetEngineParameter(SRAL.NoneEngine, SRAL.VoiceIndexParam, &original_voice_index) + if original_voice_index < 0 || original_voice_index > voice_count-1 { + fmt.Printf(" Current voice index: %d (Unknown/default)\n", original_voice_index) + } else { + fmt.Printf(" Current voice index: %d (%s)\n", original_voice_index, voice_infos[original_voice_index].Name) + } + + if voice_count > 1 { + new_voice_index := (original_voice_index + 1) % voice_count + fmt.Printf(" Attempting to set voice to index: %d (%s)\n", new_voice_index, voice_infos[new_voice_index].Name) + if SRAL.SetEngineParameter(SRAL.NoneEngine, SRAL.VoiceIndexParam, new_voice_index) { + SRAL.GetEngineParameter(SRAL.NoneEngine, SRAL.VoiceIndexParam, ¤t_voice_index) + fmt.Printf(" New voice index confirmed by get: %d\n", current_voice_index) + Check(current_voice_index == new_voice_index, "Voice index set and get matches", "Voice index set/get mismatch") + SRAL.Speak("Testing newly selected voice.", true) + time.Sleep(3000 * time.Millisecond) + if original_voice_index != -1 { + SRAL.SetEngineParameter(SRAL.NoneEngine, SRAL.VoiceIndexParam, original_voice_index) + fmt.Printf(" Restored original voice index to: %d\n", original_voice_index) + } + } else { + fmt.Printf(" Failed to set VOICE_INDEX.\n") + } + } + } + // free no required + } + } else { + fmt.Printf(" Failed to get VOICE_COUNT.\n") + } + } else { + fmt.Printf("\nVOICE selection not supported by current default engine.\n") + } + + fmt.Printf("\nTesting ENABLE_SPELLING (Default Engine):\n") + var spelling_enabled, original_spelling_state bool + if SRAL.GetEngineParameter(SRAL.NoneEngine, SRAL.EnableSpellingParam, &original_spelling_state) { + fmt.Printf(" Initial spelling state: %s\n", flagStatus(original_spelling_state)) + new_spelling_state := !original_spelling_state + fmt.Printf(" Attempting to set spelling to: %s\n", flagStatus(new_spelling_state)) + if SRAL.SetEngineParameter(SRAL.NoneEngine, SRAL.EnableSpellingParam, new_spelling_state) { + SRAL.GetEngineParameter(SRAL.NoneEngine, SRAL.EnableSpellingParam, &spelling_enabled) + fmt.Printf(" New spelling state confirmed by get: %s\n", flagStatus(spelling_enabled)) + Check(spelling_enabled == new_spelling_state, "Spelling state set/get matches", "Spelling state set/get mismatch") + if new_spelling_state { + SRAL.Speak("Spelling test: H E L L O", true) + } else { + SRAL.Speak("Spelling test: Hello", true) + } + time.Sleep(3000 * time.Millisecond) + SRAL.SetEngineParameter(SRAL.NoneEngine, SRAL.EnableSpellingParam, original_spelling_state) + fmt.Printf(" Restored original spelling state.\n") + } else { + fmt.Printf(" Failed to set ENABLE_SPELLING (This might be normal if not truly settable by this engine, or if feature isn't fully supported).\n") + } + } else { + fmt.Printf(" Failed to get initial ENABLE_SPELLING (This might be normal if not supported by current engine).\n") + } + + if current_engine_id == SRAL.NVDAEngine || (specific_engine_for_ex_tests != SRAL.NoneEngine && specific_engine_for_ex_tests == SRAL.NVDAEngine) { + fmt.Printf("\nTesting NVDA_IS_CONTROL_EX (for NVDA):\n") + var nvda_is_control_ex_val bool + if SRAL.GetEngineParameter(SRAL.NVDAEngine, SRAL.NVDAIsControlExParam, &nvda_is_control_ex_val) { + status := "false (standard control)" + if nvda_is_control_ex_val { + status = "true (extended control available)" + } + fmt.Printf(" NVDA_IS_CONTROL_EX value from NVDA engine: %s\n", status) + } else { + fmt.Printf(" Failed to get NVDA_IS_CONTROL_EX (NVDA might not be the one responding or parameter not available).\n") + } + } + + if current_engine_id == SRAL.SAPIEngine || (specific_engine_for_ex_tests != SRAL.NoneEngine && specific_engine_for_ex_tests == SRAL.SAPIEngine) { + fmt.Printf("\nTesting SAPI_TRIM_THRESHOLD (for SAPI):\n") + var trim_threshold, original_trim_threshold int + if SRAL.GetEngineParameter(SRAL.SAPIEngine, SRAL.SAPITrimThresholdParam, &original_trim_threshold) { + fmt.Printf(" Initial SAPI_TRIM_THRESHOLD: %d\n", original_trim_threshold) + new_threshold := 20 + if original_trim_threshold == 20 { + new_threshold = 60 + } + fmt.Printf(" Attempting to set SAPI_TRIM_THRESHOLD to: %d\n", new_threshold) + if SRAL.SetEngineParameter(SRAL.SAPIEngine, SRAL.SAPITrimThresholdParam, new_threshold) { + SRAL.GetEngineParameter(SRAL.SAPIEngine, SRAL.SAPITrimThresholdParam, &trim_threshold) + fmt.Printf(" New SAPI_TRIM_THRESHOLD confirmed by get: %d\n", trim_threshold) + Check(trim_threshold == new_threshold, "SAPI_TRIM_THRESHOLD set/get matches", "SAPI_TRIM_THRESHOLD set/get mismatch") + SRAL.SpeakEx(SRAL.SAPIEngine, "Testing SAPI trim threshold. ... Some silence here.", true) + time.Sleep(3000 * time.Millisecond) + SRAL.SetEngineParameter(SRAL.SAPIEngine, SRAL.SAPITrimThresholdParam, original_trim_threshold) + fmt.Printf(" Restored SAPI_TRIM_THRESHOLD.\n") + } else { + fmt.Printf(" Failed to set SAPI_TRIM_THRESHOLD.\n") + } + } else { + fmt.Printf(" Failed to get initial SAPI_TRIM_THRESHOLD (SAPI might not be responding or param not available).\n") + } + } + + fmt.Printf("\nTesting SYMBOL_LEVEL (Default Engine):\n") + var symbol_level, original_symbol_level int + if SRAL.GetEngineParameter(SRAL.NoneEngine, SRAL.SymbolLevelParam, &original_symbol_level) { + fmt.Printf(" Original symbol_level: %d\n", original_symbol_level) + new_symbol_level := (original_symbol_level + 1) % 4 + fmt.Printf(" Attempting to set symbol_level to: %d\n", new_symbol_level) + if SRAL.SetEngineParameter(SRAL.NoneEngine, SRAL.SymbolLevelParam, new_symbol_level) { + SRAL.GetEngineParameter(SRAL.NoneEngine, SRAL.SymbolLevelParam, &symbol_level) + fmt.Printf(" New symbol_level confirmed by get: %d\n", symbol_level) + Check(symbol_level == new_symbol_level, "Symbol level set/get matches", "Symbol level set/get mismatch") + SRAL.Speak("Testing symbol level with punctuation! At symbol @ hash # dollar $ percent % caret ^ ampersand & asterisk *.", true) + time.Sleep(4000 * time.Millisecond) + SRAL.SetEngineParameter(SRAL.NoneEngine, SRAL.SymbolLevelParam, original_symbol_level) + fmt.Printf(" Restored original symbol_level: %d\n", original_symbol_level) + } else { + fmt.Printf(" Failed to set SYMBOL_LEVEL (might not be supported or settable by this engine).\n") + } + } else { + fmt.Printf(" Failed to get initial SYMBOL_LEVEL (might not be supported by current engine).\n") + } + + fmt.Printf("\nTesting USE_CHARACTER_DESCRIPTIONS (Default Engine):\n") + var use_char_desc, original_use_char_desc bool + if SRAL.GetEngineParameter(SRAL.NoneEngine, SRAL.UseCharacterDescriptionsParam, &original_use_char_desc) { + fmt.Printf(" Initial USE_CHARACTER_DESCRIPTIONS: %s\n", flagStatus(original_use_char_desc)) + new_use_char_desc := !original_use_char_desc + fmt.Printf(" Attempting to set USE_CHARACTER_DESCRIPTIONS to: %s\n", flagStatus(new_use_char_desc)) + if SRAL.SetEngineParameter(SRAL.NoneEngine, SRAL.UseCharacterDescriptionsParam, new_use_char_desc) { + SRAL.GetEngineParameter(SRAL.NoneEngine, SRAL.UseCharacterDescriptionsParam, &use_char_desc) + fmt.Printf(" New USE_CHARACTER_DESCRIPTIONS confirmed by get: %s\n", flagStatus(use_char_desc)) + Check(use_char_desc == new_use_char_desc, "USE_CHARACTER_DESCRIPTIONS set/get matches", "USE_CHARACTER_DESCRIPTIONS set/get mismatch") + temp_spelling_state := true + SRAL.SetEngineParameter(SRAL.NoneEngine, SRAL.EnableSpellingParam, temp_spelling_state) + SRAL.Speak("Testing character descriptions: A B C", true) + time.Sleep(3000 * time.Millisecond) + SRAL.SetEngineParameter(SRAL.NoneEngine, SRAL.UseCharacterDescriptionsParam, original_use_char_desc) + fmt.Printf(" Restored original USE_CHARACTER_DESCRIPTIONS.\n") + } else { + fmt.Printf(" Failed to set USE_CHARACTER_DESCRIPTIONS (might not be supported or settable).\n") + } + } else { + fmt.Printf(" Failed to get initial USE_CHARACTER_DESCRIPTIONS (might not be supported).\n") + } + + TestSection("SRAL_Delay") + promptUser("Prepare for delay test. 'First message.' will speak, then a 3-second delay, then 'Second message after delay.'") + SRAL.Speak("First message.", true) + SRAL.Delay(3000 * time.Millisecond) + SRAL.Speak("Second message after delay.", true) + promptUser("Delay test finished. Press Enter to continue.") + SRAL.StopSpeech() + + TestSection("SRAL_Set/GetEnginesExclude") + + original_engines_to_exclude := engines_to_exclude + engines_to_exclude = SRAL.NVDAEngine | SRAL.SAPIEngine + fmt.Printf("Original excluding engines: %d (%s)\n", + original_engines_to_exclude, SRAL.GetEngineName(original_engines_to_exclude)) + + fmt.Printf("Attempting to exclude engines: %d (%s)\n", + engines_to_exclude, SRAL.GetEngineName(engines_to_exclude)) + + Check(SRAL.SetEnginesExclude(SRAL.NVDAEngine|SRAL.SAPIEngine), "Excludes was successfully set.", "Failed to set engines excludes") + new_engines_to_exclude := SRAL.GetEnginesExclude() + fmt.Printf(" New excludes confirmed by get: %d (%s)\n", + engines_to_exclude, SRAL.GetEngineName(new_engines_to_exclude)) + + Check(engines_to_exclude == new_engines_to_exclude, "Engines exclude set/get matches", "Engines exclude set/get mismatch") + + TestSection("Unregister Keyboard Hooks") + SRAL.UnregisterKeyboardHooks() + fmt.Printf("SRAL_UnregisterKeyboardHooks called. Hooks should now be inactive (if they were active).\n") + promptUser("Try Ctrl/Shift with next speech to confirm hooks are off (if they were previously working).") + SRAL.Speak("Testing speech output after attempting to unregister keyboard hooks.", true) + time.Sleep(3000 * time.Millisecond) + + TestSection("SRAL_Uninitialize") + SRAL.Uninitialize() + fmt.Printf("SRAL_Uninitialize called.\n") + Check(!SRAL.IsInitialized(), "SRAL_IsInitialized correctly returns false after uninit.", "SRAL_IsInitialized returned true after uninit!") + + fmt.Printf("\nAttempting to speak after uninitialization (should fail or do nothing):\n") + if SRAL.Speak("This speech should not happen.", false) { + fmt.Printf("[WARNING] SRAL_Speak appeared to succeed after uninitialization!\n") + } else { + fmt.Printf("[INFO] SRAL_Speak correctly failed or did nothing after uninitialization.\n") + } + + promptUser("All tests complete. Press Enter to exit.") +} diff --git a/Bindings/go/go.work b/Bindings/go/go.work new file mode 100644 index 0000000..b34974f --- /dev/null +++ b/Bindings/go/go.work @@ -0,0 +1,6 @@ +go 1.26.1 + +use ( + ./SRAL + ./SRAL_example +) diff --git a/CMakeLists.txt b/CMakeLists.txt index 50b788c..ecb1542 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -41,7 +41,7 @@ if(WIN32) endif() elseif(APPLE) target_sources(${PROJECT_NAME}_obj PRIVATE - "SRC/AVSpeech.h" "SRC/AVSpeech.mm" "SRC/VoiceOver.h" "SRC/VoiceOver.mm") + "SRC/AVSpeech.h" "SRC/AVSpeech.mm" "SRC/NSSpeech.h" "SRC/NSSpeech.mm" "SRC/VoiceOver.h" "SRC/VoiceOver.mm") else() target_sources(${PROJECT_NAME}_obj PRIVATE "Dep/utf-8.h" "Dep/utf-8.c" "SRC/SpeechDispatcher.h" "SRC/SpeechDispatcher.cpp") diff --git a/Include/SRAL.h b/Include/SRAL.h index 828136c..cb3a0ca 100644 --- a/Include/SRAL.h +++ b/Include/SRAL.h @@ -75,12 +75,17 @@ SRAL_ENGINE_SAPI = 1 << 6, 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 << 8, + +SRAL_ENGINE_VOICE_OVER = 1 << 9, // --- Apple Speech Synthesis Engines (macOS, iOS, etc.) --- /** @brief AVFoundation Speech Synthesizer (AVSpeechSynthesizer), for text-to-speech on Apple platforms. */ -SRAL_ENGINE_AV_SPEECH = 1 << 9 +SRAL_ENGINE_AV_SPEECH = 1 << 10 }; /** diff --git a/SRC/AVSpeech.mm b/SRC/AVSpeech.mm index 3543a35..08f103e 100644 --- a/SRC/AVSpeech.mm +++ b/SRC/AVSpeech.mm @@ -31,17 +31,23 @@ bool Initialize() { bool Uninitialize(){ return true; } + bool Speak(const char* text, bool interrupt){ - if (interrupt && synth.isSpeaking)[synth stopSpeakingAtBoundary:AVSpeechBoundaryImmediate]; - NSString *nstext = [NSString stringWithUTF8String:text]; - AVSpeechUtterance *utterance = [[AVSpeechUtterance alloc] initWithString:nstext]; - utterance.rate = rate; - utterance.volume = volume; - utterance.voice = currentVoice; - this->utterance = utterance; - [synth speakUtterance:this->utterance]; - return synth.isSpeaking; + if (synth == nil) return false; + if (interrupt && synth.isSpeaking) { + [synth stopSpeakingAtBoundary:AVSpeechBoundaryImmediate]; + } + NSString *nstext = [NSString stringWithUTF8String:text]; + if (nstext == nil) return false; + AVSpeechUtterance *utterance = [[AVSpeechUtterance alloc] initWithString:nstext]; + utterance.rate = rate; + utterance.volume = volume; + utterance.voice = currentVoice; + this->utterance = utterance; + [synth speakUtterance:this->utterance]; + return true; // because IsSpeaking hasn't changed yet } + bool StopSpeech(){ if (synth.isSpeaking) return [synth stopSpeakingAtBoundary:AVSpeechBoundaryImmediate]; return false; diff --git a/SRC/NSSpeech.h b/SRC/NSSpeech.h new file mode 100644 index 0000000..e489005 --- /dev/null +++ b/SRC/NSSpeech.h @@ -0,0 +1,27 @@ +#pragma once +#include "../Include/SRAL.h" +#include "Engine.h" + +class NSSpeechSynthesizerWrapper; + +namespace Sral { + class NsSpeech final : public Engine { + public: + bool Initialize() override; + bool Uninitialize() override; + bool Speak(const char* text, bool interrupt) override; + bool StopSpeech() override; + bool IsSpeaking() override; + bool GetActive() override; + bool SetParameter(int param, const void* value) override; + bool GetParameter(int param, void* value) override; + + int GetNumber() override { return SRAL_ENGINE_NS_SPEECH; } + int GetFeatures() override { + return SRAL_SUPPORTS_SPEECH | SRAL_SUPPORTS_SPEECH_RATE | SRAL_SUPPORTS_SPEECH_VOLUME; + } + + private: + NSSpeechSynthesizerWrapper* obj = nullptr; + }; +} diff --git a/SRC/NSSpeech.mm b/SRC/NSSpeech.mm new file mode 100644 index 0000000..9d59458 --- /dev/null +++ b/SRC/NSSpeech.mm @@ -0,0 +1,110 @@ +#include "NsSpeech.h" +#import + +class NSSpeechSynthesizerWrapper { +public: + NSSpeechSynthesizer* synth; + float rate; + float volume; + + NSSpeechSynthesizerWrapper() : synth(nullptr), rate(175.0f), volume(1.0f) {} + + bool Initialize() { + synth = [[NSSpeechSynthesizer alloc] init]; + if (synth) { + rate = [synth rate]; + volume = [synth volume]; + } + return synth != nil; + } + + void Uninitialize() { + if (synth) { + [synth stopSpeaking]; + [synth release]; + synth = nil; + } + } + + bool Speak(const char* text, bool interrupt) { + if (!synth) return false; + if (interrupt) [synth stopSpeaking]; + + NSString* nsStr = [NSString stringWithUTF8String:text]; + return [synth startSpeakingString:nsStr] == YES; + } + + bool Stop() { + if (synth) [synth stopSpeaking]; + return true; + } + + bool IsSpeaking() { + return synth && [synth isSpeaking]; + } + + void SetVolume(int val) { + this->volume = (float)val / 100.0f; + if (synth) [synth setVolume:this->volume]; + } + + void SetRate(int val) { + this->rate = (float)val; + if (synth) [synth setRate:this->rate]; + } +}; + +namespace Sral { + + bool NsSpeech::Initialize() { + obj = new NSSpeechSynthesizerWrapper(); + return obj->Initialize(); +} + +bool NsSpeech::Uninitialize() { + if (obj) { + obj->Uninitialize(); + delete obj; + obj = nullptr; + } + return true; +} + +bool NsSpeech::Speak(const char* text, bool interrupt) { + return obj ? obj->Speak(text, interrupt) : false; +} + +bool NsSpeech::StopSpeech() { + return obj ? obj->Stop() : false; +} + +bool NsSpeech::IsSpeaking() { + return obj ? obj->IsSpeaking() : false; +} + +bool NsSpeech::GetActive() { + return obj != nullptr; +} + +bool NsSpeech::SetParameter(int param, const void* value) { + if (!obj) return false; + int val = *reinterpret_cast(value); + switch (param) { + case SRAL_PARAM_SPEECH_RATE: obj->SetRate(val); break; + case SRAL_PARAM_SPEECH_VOLUME: obj->SetVolume(val); break; + default: return false; + } + return true; +} + +bool NsSpeech::GetParameter(int param, void* value) { + if (!obj) return false; + switch (param) { + case SRAL_PARAM_SPEECH_RATE: *(int*)value = (int)obj->rate; break; + case SRAL_PARAM_SPEECH_VOLUME: *(int*)value = (int)(obj->volume * 100); break; + default: return false; + } + return true; +} + +} // namespace Sral diff --git a/SRC/SRAL.cpp b/SRC/SRAL.cpp index 1a45572..9d62376 100644 --- a/SRC/SRAL.cpp +++ b/SRC/SRAL.cpp @@ -14,6 +14,7 @@ #include #elif defined(__APPLE__) #include "AVSpeech.h" +#include "NSSpeech.h" #include "VoiceOver.h" #else #include "SpeechDispatcher.h" @@ -228,6 +229,7 @@ extern "C" SRAL_API bool SRAL_Initialize(int engines_exclude) { #elif defined(__APPLE__) g_engines[SRAL_ENGINE_VOICE_OVER] = std::make_unique(); g_engines[SRAL_ENGINE_AV_SPEECH] = std::make_unique(); + g_engines[SRAL_ENGINE_NS_SPEECH] = std::make_unique(); #else g_engines[SRAL_ENGINE_SPEECH_DISPATCHER] = std::make_unique(); #endif @@ -606,6 +608,7 @@ extern "C" SRAL_API const char* SRAL_GetEngineName(int engine) { case SRAL_ENGINE_SPEECH_DISPATCHER: return "Speech Dispatcher"; case SRAL_ENGINE_UIA: return "UIA"; case SRAL_ENGINE_AV_SPEECH: return "AV Speech"; + case SRAL_ENGINE_NS_SPEECH: return "NS Speech"; case SRAL_ENGINE_NARRATOR: return "Narrator"; case SRAL_ENGINE_VOICE_OVER: return "Voice Over"; case SRAL_ENGINE_ZDSR: return "ZDSR"; diff --git a/meson.build b/meson.build new file mode 100644 index 0000000..e026789 --- /dev/null +++ b/meson.build @@ -0,0 +1,85 @@ +project('SRAL', ['c', 'cpp'], + version : '1.0.0', + default_options : [ + 'cpp_std=c++20', + 'default_library=both', + 'warning_level=2' + ] +) + +cc = meson.get_compiler('c') +cpp = meson.get_compiler('cpp') +host_os = host_machine.system() + +inc_dir = include_directories('Include') + +build_test = get_option('build_sral_test') +disable_uia = get_option('sral_disable_uia') + +sral_sources = [ + 'SRC/Encoding.cpp', + 'SRC/SRAL.cpp', + 'SRC/Engine.cpp' +] + +sral_deps = [] +sral_args = ['-D_CRT_SECURE_NO_WARNINGS'] + +if host_os == 'windows' + sral_sources += [ + 'SRC/NVDA.cpp', 'SRC/SAPI.cpp', + 'Dep/blastspeak.c', 'Dep/fsapi.c', 'Dep/nvda_control.c', + 'SRC/Jaws.cpp', 'Dep/wasapi.cpp', 'SRC/ZDSR.cpp' + ] + + if not disable_uia + sral_sources += ['SRC/UIA.cpp', 'Dep/UIAProvider.cpp'] + sral_deps += cpp.find_library('uiautomationcore') + else + sral_args += '-DSRAL_NO_UIA' + endif + +elif host_os == 'darwin' + add_languages('objcpp') + sral_sources += ['SRC/AVSpeech.mm', 'SRC/VoiceOver.mm', 'SRC/NSSpeech.mm'] + sral_deps += dependency('appleframeworks', modules : ['AppKit', 'Foundation', 'AVFoundation']) + +else + sral_sources += ['Dep/utf-8.c', 'SRC/SpeechDispatcher.cpp'] + sral_deps += dependency('speech-dispatcher') + sral_deps += cpp.find_library('brlapi') +endif + +sral_lib = library('SRAL', + sral_sources, + include_directories : inc_dir, + dependencies : sral_deps, + cpp_args : sral_args, + c_args : sral_args, + install : true +) + +install_headers('Include/SRAL.h') + +if build_test + executable('SRAL_test', + 'Examples/C/SRALExample.c', + include_directories : inc_dir, + link_with : sral_lib, + dependencies : sral_deps + ) + + if host_os == 'windows' + executable('SRAL_NVDAControleExConsole', + ['Examples/C/NVDAControlExConsole.c', 'Dep/nvda_control.c'], + include_directories : inc_dir + ) + endif +endif + +summary({ + 'Build tests': build_test, + 'UIA support disabled': disable_uia, + 'Library type': get_option('default_library'), + 'C++ Standard': get_option('cpp_std') +}, bool_yn: true, section: 'Configuration') diff --git a/meson_options.txt b/meson_options.txt new file mode 100644 index 0000000..c758c2c --- /dev/null +++ b/meson_options.txt @@ -0,0 +1,2 @@ +option('build_sral_test', type : 'boolean', value : true, description : 'Build SRAL examples/tests') +option('sral_disable_uia', type : 'boolean', value : false, description : 'Disable UIA (UI Automation) support') \ No newline at end of file diff --git a/windows-mingw-cross.txt b/windows-mingw-cross.txt new file mode 100644 index 0000000..00f014f --- /dev/null +++ b/windows-mingw-cross.txt @@ -0,0 +1,13 @@ +[binaries] +c = 'x86_64-w64-mingw32-gcc' +cpp = 'x86_64-w64-mingw32-g++' +ar = 'x86_64-w64-mingw32-ar' +strip = 'x86_64-w64-mingw32-strip' +windres = 'x86_64-w64-mingw32-windres' +exe_wrapper = 'wine' + +[host_machine] +system = 'windows' +cpu_family = 'x86_64' +cpu = 'x86_64' +endian = 'little' \ No newline at end of file