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
9 changes: 9 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ elseif(APPLE)
target_sources(${PROJECT_NAME}_obj PRIVATE
"SRC/NSSpeech.h" "SRC/NSSpeech.mm")
endif()
elseif(ANDROID)
target_sources(${PROJECT_NAME}_obj PRIVATE
"SRC/AndroidTextToSpeech.h" "SRC/AndroidTextToSpeech.cpp"
"Dep/AndroidContext.h" "Dep/AndroidContext.cpp")
else()
target_sources(${PROJECT_NAME}_obj PRIVATE
"Dep/utf-8.h" "Dep/utf-8.c" "SRC/SpeechDispatcher.h" "SRC/SpeechDispatcher.cpp")
Expand Down Expand Up @@ -135,6 +139,11 @@ if (BUILD_SRAL_TEST)
"-framework AVFoundation"
)
endif()
elseif(ANDROID)
if(BUILD_SHARED_LIBS)
target_link_libraries(${PROJECT_NAME} log)
endif()
target_link_libraries(${PROJECT_NAME}_static log)
else()
find_package(PkgConfig REQUIRED)
pkg_check_modules(SpeechD REQUIRED speech-dispatcher)
Expand Down
55 changes: 55 additions & 0 deletions Dep/AndroidContext.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#include "AndroidContext.h"

namespace Sral {

namespace {
JavaVM* g_vm = nullptr;
jobject g_activity = nullptr;
}

bool SetAndroidJNIEnv(JNIEnv* env) {
if (!env) return false;
return env->GetJavaVM(&g_vm) == JNI_OK;
}

bool SetAndroidActivity(jobject activity) {
if (!activity) return false;
JNIEnv* env = GetAndroidJNIEnv();
if (!env) return false;
if (g_activity) {
env->DeleteGlobalRef(g_activity);
g_activity = nullptr;
}
g_activity = env->NewGlobalRef(activity);
return g_activity != nullptr;
}

void ClearAndroidContext() {
JNIEnv* env = GetAndroidJNIEnv();
if (env && g_activity) {
env->DeleteGlobalRef(g_activity);
}
g_activity = nullptr;
g_vm = nullptr;
}

// A JNIEnv* is thread-local — the one passed to SetAndroidJNIEnv is only
// valid on the thread that called into SRAL. We cache the JavaVM*
// (process-global) instead and use GetEnv/AttachCurrentThread to obtain a
// valid JNIEnv* for whichever thread is currently calling into SRAL.
JNIEnv* GetAndroidJNIEnv() {
if (!g_vm) return nullptr;
JNIEnv* env = nullptr;
jint status = g_vm->GetEnv((void**)&env, JNI_VERSION_1_6);
if (status == JNI_OK) return env;
if (status == JNI_EDETACHED) {
if (g_vm->AttachCurrentThread(&env, nullptr) == JNI_OK) return env;
}
return nullptr;
}

jobject GetAndroidActivity() {
return g_activity;
}

} // namespace Sral
42 changes: 42 additions & 0 deletions Dep/AndroidContext.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#ifndef SRAL_ANDROID_CONTEXT_H_
#define SRAL_ANDROID_CONTEXT_H_
#pragma once
#include <jni.h>

// Shared Android JNI context for SRAL engines.
//
// Native code on Android cannot obtain a JNIEnv* or the app's Activity on its
// own — the host application must provide them. The host sets them via
// SRAL_SetEngineParameter using SRAL_PARAM_ANDROID_JNI_ENV and
// SRAL_PARAM_ANDROID_ACTIVITY (see SRAL.h), which forward into the setters
// below. Engine implementations (e.g. AndroidTextToSpeech, future
// AndroidTalkBack) retrieve the values through the accessor functions.
//
// SetAndroidJNIEnv must be called before SetAndroidActivity, since creating
// a global ref for the activity requires a valid JNIEnv*.

namespace Sral {

// Captures the JavaVM* from the provided env. Returns false if env is null.
bool SetAndroidJNIEnv(JNIEnv* env);

// Stores a global ref to activity. Requires SetAndroidJNIEnv to have been
// called first. Returns false if the JavaVM is not yet set, activity is null,
// or the global ref could not be created.
bool SetAndroidActivity(jobject activity);

// Called by SRAL_Uninitialize. Releases the global ref to activity.
void ClearAndroidContext();

// Returns a JNIEnv* valid on the calling thread, attaching the thread to
// the JavaVM if necessary. Returns nullptr if SetAndroidJNIEnv has not
// been called or if attach fails.
JNIEnv* GetAndroidJNIEnv();

// Returns the Activity global ref, or nullptr if SetAndroidActivity has
// not been called.
jobject GetAndroidActivity();

} // namespace Sral

#endif
66 changes: 66 additions & 0 deletions Dep/AndroidTTSHelper.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package org.sral;

import android.content.Context;
import android.os.Bundle;
import android.speech.tts.TextToSpeech;
import android.speech.tts.UtteranceProgressListener;
import java.util.Locale;

public class AndroidTTSHelper {
private TextToSpeech tts;
private boolean ready = false;
private boolean speaking = false;
private float currentRate = 1.0f;
private float currentVolume = 1.0f;

public AndroidTTSHelper(Context context) {
tts = new TextToSpeech(context, status -> {
if (status == TextToSpeech.SUCCESS) {
tts.setLanguage(Locale.getDefault());
tts.setOnUtteranceProgressListener(new UtteranceProgressListener() {
@Override public void onStart(String utteranceId) { speaking = true; }
@Override public void onDone(String utteranceId) { speaking = false; }
@Override public void onError(String utteranceId) { speaking = false; }
});
ready = true;
}
});
}

public boolean isActive() { return ready; }

public boolean isSpeaking() { return speaking; }

public void speak(String text, boolean interrupt) {
if (!ready) return;
int queueMode = interrupt ? TextToSpeech.QUEUE_FLUSH : TextToSpeech.QUEUE_ADD;
Bundle params = new Bundle();
tts.speak(text, queueMode, params, "sral_utterance");
}

public void stop() {
if (tts != null) tts.stop();
speaking = false;
}

public void setSpeechRate(float rate) {
if (tts != null) tts.setSpeechRate(rate);
currentRate = rate;
}

public void setVolume(float volume) {
currentVolume = volume;
}

public float getRate() { return currentRate; }
public float getVolume() { return currentVolume; }

public void shutdown() {
if (tts != null) {
tts.stop();
tts.shutdown();
tts = null;
}
ready = false;
}
}
21 changes: 19 additions & 2 deletions Include/SRAL.h
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,10 @@ SRAL_ENGINE_VOICE_OVER = 1 << 8,
SRAL_ENGINE_NS_SPEECH = 1 << 9,

/** @brief AVFoundation Speech Synthesizer (AVSpeechSynthesizer), for text-to-speech on Apple platforms. */
SRAL_ENGINE_AV_SPEECH = 1 << 10
SRAL_ENGINE_AV_SPEECH = 1 << 10,

// --- Android Speech Synthesis Engines ---
SRAL_ENGINE_ANDROID_TEXT_TO_SPEECH = 1 << 11
};

/**
Expand Down Expand Up @@ -122,7 +125,21 @@ SRAL_ENGINE_AV_SPEECH = 1 << 10
SRAL_PARAM_SAPI_TRIM_THRESHOLD,
SRAL_PARAM_ENABLE_SPELLING,
SRAL_PARAM_USE_CHARACTER_DESCRIPTIONS,
SRAL_PARAM_NVDA_IS_CONTROL_EX
SRAL_PARAM_NVDA_IS_CONTROL_EX,

/**
* @brief (Android only) Set the JNIEnv* used by Android engines.
* Must be set via SRAL_SetEngineParameter before SRAL_Initialize.
* Value is a JNIEnv* cast to void*.
*/
SRAL_PARAM_ANDROID_JNI_ENV,

/**
* @brief (Android only) Set the Activity (jobject) used by Android engines.
* Must be set via SRAL_SetEngineParameter before SRAL_Initialize.
* Value is a jobject cast to void*.
*/
SRAL_PARAM_ANDROID_ACTIVITY
};


Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,15 @@ SRAL is a cross-platform library designed to provide a unified interface for out

## 🛠 Supported Engines & Platforms

SRAL supports Windows, macOS, and Linux.
SRAL supports Windows, macOS, iOS, Android, and Linux.

| Category | Supported Engines |
| --- | --- |
| **Windows Screen Readers** | NVDA, JAWS, ZDSR, Microsoft Narrator |
| **Windows Frameworks** | Microsoft UI Automation (UIA) |
| **Apple (macOS)** | VoiceOver, AVFoundation (AVSpeech) |
| **macOS** | VoiceOver, NSSpeech, AVFoundation (AVSpeech) |
| **iOS** | VoiceOver, AVFoundation (AVSpeech) |
| **Android** | Android TextToSpeech |
| **Linux** | Speech Dispatcher |
| **General APIs** | Microsoft SAPI (Windows), BRLTTY (Braille) |

Expand Down Expand Up @@ -121,5 +123,3 @@ bool SRAL_Braille(const char* text);
bool SRAL_IsSpeaking(void);

```


120 changes: 120 additions & 0 deletions SRC/AndroidTextToSpeech.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
#include "AndroidTextToSpeech.h"
#include "../Dep/AndroidContext.h"

namespace Sral {

bool AndroidTextToSpeech::Initialize() {
env = GetAndroidJNIEnv();
if (!env) return false;

jobject activity = GetAndroidActivity();
if (!activity) return false;

speechClass = env->FindClass("org/sral/AndroidTTSHelper");
if (!speechClass || env->ExceptionCheck()) {
env->ExceptionClear();
return false;
}
speechClass = (jclass)env->NewGlobalRef(speechClass);

constructor = env->GetMethodID(speechClass, "<init>", "(Landroid/content/Context;)V");
midSpeak = env->GetMethodID(speechClass, "speak", "(Ljava/lang/String;Z)V");
midSilence = env->GetMethodID(speechClass, "stop", "()V");
midIsActive = env->GetMethodID(speechClass, "isActive", "()Z");
midIsSpeaking = env->GetMethodID(speechClass, "isSpeaking", "()Z");
midSetRate = env->GetMethodID(speechClass, "setSpeechRate", "(F)V");
midSetVolume = env->GetMethodID(speechClass, "setVolume", "(F)V");
midGetRate = env->GetMethodID(speechClass, "getRate", "()F");
midGetVolume = env->GetMethodID(speechClass, "getVolume", "()F");

if (!constructor || !midSpeak || !midSilence || !midIsActive || !midIsSpeaking) return false;

jobject localObj = env->NewObject(speechClass, constructor, activity);
if (!localObj) return false;
speechObj = env->NewGlobalRef(localObj);
env->DeleteLocalRef(localObj);

return true;
}

bool AndroidTextToSpeech::Uninitialize() {
ReleaseAllStrings();
if (env && speechObj) {
jmethodID midShutdown = env->GetMethodID(speechClass, "shutdown", "()V");
if (midShutdown) env->CallVoidMethod(speechObj, midShutdown);
env->DeleteGlobalRef(speechObj);
speechObj = nullptr;
}
if (env && speechClass) {
env->DeleteGlobalRef(speechClass);
speechClass = nullptr;
}
return true;
}

bool AndroidTextToSpeech::GetActive() {
if (!env || !speechObj || !midIsActive) return false;
return env->CallBooleanMethod(speechObj, midIsActive);
}

bool AndroidTextToSpeech::Speak(const char* text, bool interrupt) {
if (!env || !speechObj || !midSpeak) return false;
jstring jtext = env->NewStringUTF(text);
if (!jtext) return false;
env->CallVoidMethod(speechObj, midSpeak, jtext, (jboolean)interrupt);
env->DeleteLocalRef(jtext);
return true;
}

bool AndroidTextToSpeech::StopSpeech() {
if (!env || !speechObj || !midSilence) return false;
env->CallVoidMethod(speechObj, midSilence);
return true;
}

bool AndroidTextToSpeech::IsSpeaking() {
if (!env || !speechObj || !midIsSpeaking) return false;
return env->CallBooleanMethod(speechObj, midIsSpeaking);
}

bool AndroidTextToSpeech::SetParameter(int param, const void* value) {
if (!env || !speechObj) return false;
switch (param) {
case SRAL_PARAM_SPEECH_RATE:
if (midSetRate) {
env->CallVoidMethod(speechObj, midSetRate, (jfloat)*reinterpret_cast<const int*>(value));
return true;
}
return false;
case SRAL_PARAM_SPEECH_VOLUME:
if (midSetVolume) {
env->CallVoidMethod(speechObj, midSetVolume, (jfloat)*reinterpret_cast<const int*>(value));
return true;
}
return false;
default:
return false;
}
}

bool AndroidTextToSpeech::GetParameter(int param, void* value) {
if (!env || !speechObj) return false;
switch (param) {
case SRAL_PARAM_SPEECH_RATE:
if (midGetRate) {
*(int*)value = (int)env->CallFloatMethod(speechObj, midGetRate);
return true;
}
return false;
case SRAL_PARAM_SPEECH_VOLUME:
if (midGetVolume) {
*(int*)value = (int)env->CallFloatMethod(speechObj, midGetVolume);
return true;
}
return false;
default:
return false;
}
}

} // namespace Sral
2 changes: 1 addition & 1 deletion SRC/AndroidTextToSpeech.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ namespace Sral {
bool SpeakSsml(const char* ssml, bool interrupt)override {
return false;
}
void* SpeakToMemory(const char* text, uint64_t* buffer_size, int* channels, int* sample_rate, int* bits_per_sample)override { return false;}
void* SpeakToMemory(const char* text, uint64_t* buffer_size, int* channels, int* sample_rate, int* bits_per_sample)override { return nullptr;}
bool SetParameter(int param, const void* value)override;
bool GetParameter(int param, void* value) override;

Expand Down
Loading
Loading