@@ -1372,5 +1460,17 @@
Plugged in, battery fully charged.
+
+ Utterance has begun to be spoken.
+
+
+ Utterance was successfully finished.
+
+
+ Utterance was canceled, or TTS service was unable to process it.
+
+
+ Utterance reached a word or sentence boundary.
+
diff --git a/platform/android/SCsub b/platform/android/SCsub
index f71416204..f0114392c 100644
--- a/platform/android/SCsub
+++ b/platform/android/SCsub
@@ -9,6 +9,7 @@ android_files = [
"file_access_filesystem_jandroid.cpp",
"audio_driver_opensl.cpp",
"dir_access_jandroid.cpp",
+ "tts_android.cpp",
"thread_jandroid.cpp",
"net_socket_android.cpp",
"java_pandemonium_lib_jni.cpp",
diff --git a/platform/android/java/lib/src/org/pandemoniumengine/pandemonium/Pandemonium.java b/platform/android/java/lib/src/org/pandemoniumengine/pandemonium/Pandemonium.java
index 22af80283..f843e2fab 100644
--- a/platform/android/java/lib/src/org/pandemoniumengine/pandemonium/Pandemonium.java
+++ b/platform/android/java/lib/src/org/pandemoniumengine/pandemonium/Pandemonium.java
@@ -43,6 +43,7 @@ import org.pandemoniumengine.pandemonium.plugin.PandemoniumPluginRegistry;
import org.pandemoniumengine.pandemonium.utils.BenchmarkUtils;
import org.pandemoniumengine.pandemonium.utils.PandemoniumNetUtils;
import org.pandemoniumengine.pandemonium.utils.PermissionsUtil;
+import org.pandemoniumengine.pandemonium.tts.PandemoniumTTS;
import android.annotation.SuppressLint;
import android.app.Activity;
@@ -265,6 +266,7 @@ public class Pandemonium extends Fragment implements SensorEventListener, IDownl
public PandemoniumNetUtils netUtils;
private DirectoryAccessHandler directoryAccessHandler;
private FileAccessHandler fileAccessHandler;
+ public PandemoniumTTS tts;
static SingletonBase[] singletons = new SingletonBase[MAX_SINGLETONS];
static int singleton_count = 0;
@@ -330,7 +332,7 @@ public class Pandemonium extends Fragment implements SensorEventListener, IDownl
protected void onPandemoniumSetupCompleted() {
Log.d(TAG, "onPandemoniumSetupCompleted");
- // These properties are defined after Godot setup completion, so we retrieve them here.
+ // These properties are defined after Pandemonium setup completion, so we retrieve them here.
boolean longPressEnabled = Boolean.parseBoolean(PandemoniumLib.getGlobal("input_devices/pointing/android/enable_long_press_as_right_click"));
boolean panScaleEnabled = Boolean.parseBoolean(PandemoniumLib.getGlobal("input_devices/pointing/android/enable_pan_and_scale_gestures"));
int rotaryInputAxis = java.lang.Integer.parseInt(PandemoniumLib.getGlobal("input_devices/pointing/android/rotary_input_scroll_axis"));
@@ -385,7 +387,7 @@ public class Pandemonium extends Fragment implements SensorEventListener, IDownl
PandemoniumEditText edittext = new PandemoniumEditText(activity);
edittext.setLayoutParams(new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT,
(int)getResources().getDimension(R.dimen.text_edit_height)));
- // Prevent GodotEditText from showing on splash screen on devices with Android 14 or newer.
+ // Prevent PandemoniumEditText from showing on splash screen on devices with Android 14 or newer.
edittext.setBackgroundColor(Color.TRANSPARENT);
// ...add to FrameLayout
containerLayout.addView(edittext);
@@ -709,6 +711,7 @@ public class Pandemonium extends Fragment implements SensorEventListener, IDownl
final Activity activity = getActivity();
io = new PandemoniumIO(activity);
netUtils = new PandemoniumNetUtils(activity);
+ tts = new PandemoniumTTS(activity);
Context context = getContext();
directoryAccessHandler = new DirectoryAccessHandler(context);
diff --git a/platform/android/java/lib/src/org/pandemoniumengine/pandemonium/PandemoniumLib.java b/platform/android/java/lib/src/org/pandemoniumengine/pandemonium/PandemoniumLib.java
index 29b37a677..0ac350021 100644
--- a/platform/android/java/lib/src/org/pandemoniumengine/pandemonium/PandemoniumLib.java
+++ b/platform/android/java/lib/src/org/pandemoniumengine/pandemonium/PandemoniumLib.java
@@ -91,6 +91,11 @@ public class PandemoniumLib {
*/
public static native void step();
+ /**
+ * TTS callback.
+ */
+ public static native void ttsCallback(int event, int id, int pos);
+
/**
* Forward touch events
*/
diff --git a/platform/android/java/lib/src/org/pandemoniumengine/pandemonium/tts/PandemoniumTTS.java b/platform/android/java/lib/src/org/pandemoniumengine/pandemonium/tts/PandemoniumTTS.java
new file mode 100644
index 000000000..d8816d517
--- /dev/null
+++ b/platform/android/java/lib/src/org/pandemoniumengine/pandemonium/tts/PandemoniumTTS.java
@@ -0,0 +1,298 @@
+/*************************************************************************/
+/* PandemoniumTTS.java */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+package org.pandemoniumengine.pandemonium.tts;
+
+import org.pandemoniumengine.pandemonium.PandemoniumLib;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.speech.tts.TextToSpeech;
+import android.speech.tts.UtteranceProgressListener;
+import android.speech.tts.Voice;
+
+import androidx.annotation.Keep;
+
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.Set;
+
+/**
+ * Wrapper for Android Text to Speech API and custom utterance query implementation.
+ *
+ * A [PandemoniumTTS] provides the following features:
+ *
+ *
+ * - Access to the Android Text to Speech API.
+ *
- Utterance pause / resume functions, unsupported by Android TTS API.
+ *
+ */
+@Keep
+public class PandemoniumTTS extends UtteranceProgressListener {
+ // Note: These constants must be in sync with OS::TTSUtteranceEvent enum from "core/os/os.h".
+ final private static int EVENT_START = 0;
+ final private static int EVENT_END = 1;
+ final private static int EVENT_CANCEL = 2;
+ final private static int EVENT_BOUNDARY = 3;
+
+ final private TextToSpeech synth;
+ final private LinkedList queue;
+ final private Object lock = new Object();
+ private PandemoniumUtterance lastUtterance;
+
+ private boolean speaking;
+ private boolean paused;
+
+ public PandemoniumTTS(Activity p_activity) {
+ synth = new TextToSpeech(p_activity, null);
+ queue = new LinkedList();
+
+ synth.setOnUtteranceProgressListener(this);
+ }
+
+ private void updateTTS() {
+ if (!speaking && queue.size() > 0) {
+ int mode = TextToSpeech.QUEUE_FLUSH;
+ PandemoniumUtterance message = queue.pollFirst();
+
+ Set voices = synth.getVoices();
+ for (Voice v : voices) {
+ if (v.getName().equals(message.voice)) {
+ synth.setVoice(v);
+ break;
+ }
+ }
+ synth.setPitch(message.pitch);
+ synth.setSpeechRate(message.rate);
+
+ Bundle params = new Bundle();
+ params.putFloat(TextToSpeech.Engine.KEY_PARAM_VOLUME, message.volume / 100.f);
+
+ lastUtterance = message;
+ lastUtterance.start = 0;
+ lastUtterance.offset = 0;
+ paused = false;
+
+ synth.speak(message.text, mode, params, String.valueOf(message.id));
+ speaking = true;
+ }
+ }
+
+ /**
+ * Called by TTS engine when the TTS service is about to speak the specified range.
+ */
+ @Override
+ public void onRangeStart(String utteranceId, int start, int end, int frame) {
+ synchronized (lock) {
+ if (lastUtterance != null && Integer.parseInt(utteranceId) == lastUtterance.id) {
+ lastUtterance.offset = start;
+ PandemoniumLib.ttsCallback(EVENT_BOUNDARY, lastUtterance.id, start + lastUtterance.start);
+ }
+ }
+ }
+
+ /**
+ * Called by TTS engine when an utterance was canceled in progress.
+ */
+ @Override
+ public void onStop(String utteranceId, boolean interrupted) {
+ synchronized (lock) {
+ if (lastUtterance != null && !paused && Integer.parseInt(utteranceId) == lastUtterance.id) {
+ PandemoniumLib.ttsCallback(EVENT_CANCEL, lastUtterance.id, 0);
+ speaking = false;
+ updateTTS();
+ }
+ }
+ }
+
+ /**
+ * Called by TTS engine when an utterance has begun to be spoken..
+ */
+ @Override
+ public void onStart(String utteranceId) {
+ synchronized (lock) {
+ if (lastUtterance != null && lastUtterance.start == 0 && Integer.parseInt(utteranceId) == lastUtterance.id) {
+ PandemoniumLib.ttsCallback(EVENT_START, lastUtterance.id, 0);
+ }
+ }
+ }
+
+ /**
+ * Called by TTS engine when an utterance was successfully finished.
+ */
+ @Override
+ public void onDone(String utteranceId) {
+ synchronized (lock) {
+ if (lastUtterance != null && !paused && Integer.parseInt(utteranceId) == lastUtterance.id) {
+ PandemoniumLib.ttsCallback(EVENT_END, lastUtterance.id, 0);
+ speaking = false;
+ updateTTS();
+ }
+ }
+ }
+
+ /**
+ * Called by TTS engine when an error has occurred during processing.
+ */
+ @Override
+ public void onError(String utteranceId, int errorCode) {
+ synchronized (lock) {
+ if (lastUtterance != null && !paused && Integer.parseInt(utteranceId) == lastUtterance.id) {
+ PandemoniumLib.ttsCallback(EVENT_CANCEL, lastUtterance.id, 0);
+ speaking = false;
+ updateTTS();
+ }
+ }
+ }
+
+ /**
+ * Called by TTS engine when an error has occurred during processing (pre API level 21 version).
+ */
+ @Override
+ public void onError(String utteranceId) {
+ synchronized (lock) {
+ if (lastUtterance != null && !paused && Integer.parseInt(utteranceId) == lastUtterance.id) {
+ PandemoniumLib.ttsCallback(EVENT_CANCEL, lastUtterance.id, 0);
+ speaking = false;
+ updateTTS();
+ }
+ }
+ }
+
+ /**
+ * Adds an utterance to the queue.
+ */
+ public void speak(String text, String voice, int volume, float pitch, float rate, int utterance_id, boolean interrupt) {
+ synchronized (lock) {
+ PandemoniumUtterance message = new PandemoniumUtterance(text, voice, volume, pitch, rate, utterance_id);
+ queue.addLast(message);
+
+ if (isPaused()) {
+ resumeSpeaking();
+ } else {
+ updateTTS();
+ }
+ }
+ }
+
+ /**
+ * Puts the synthesizer into a paused state.
+ */
+ public void pauseSpeaking() {
+ synchronized (lock) {
+ if (!paused) {
+ paused = true;
+ synth.stop();
+ }
+ }
+ }
+
+ /**
+ * Resumes the synthesizer if it was paused.
+ */
+ public void resumeSpeaking() {
+ synchronized (lock) {
+ if (lastUtterance != null && paused) {
+ int mode = TextToSpeech.QUEUE_FLUSH;
+
+ Set voices = synth.getVoices();
+ for (Voice v : voices) {
+ if (v.getName().equals(lastUtterance.voice)) {
+ synth.setVoice(v);
+ break;
+ }
+ }
+ synth.setPitch(lastUtterance.pitch);
+ synth.setSpeechRate(lastUtterance.rate);
+
+ Bundle params = new Bundle();
+ params.putFloat(TextToSpeech.Engine.KEY_PARAM_VOLUME, lastUtterance.volume / 100.f);
+
+ lastUtterance.start = lastUtterance.offset;
+ lastUtterance.offset = 0;
+ paused = false;
+
+ synth.speak(lastUtterance.text.substring(lastUtterance.start), mode, params, String.valueOf(lastUtterance.id));
+ speaking = true;
+ } else {
+ paused = false;
+ }
+ }
+ }
+
+ /**
+ * Stops synthesis in progress and removes all utterances from the queue.
+ */
+ public void stopSpeaking() {
+ synchronized (lock) {
+ for (PandemoniumUtterance u : queue) {
+ PandemoniumLib.ttsCallback(EVENT_CANCEL, u.id, 0);
+ }
+ queue.clear();
+
+ if (lastUtterance != null) {
+ PandemoniumLib.ttsCallback(EVENT_CANCEL, lastUtterance.id, 0);
+ }
+ lastUtterance = null;
+
+ paused = false;
+ speaking = false;
+
+ synth.stop();
+ }
+ }
+
+ /**
+ * Returns voice information.
+ */
+ public String[] getVoices() {
+ Set voices = synth.getVoices();
+ String[] list = new String[voices.size()];
+ int i = 0;
+ for (Voice v : voices) {
+ list[i++] = v.getLocale().toString() + ";" + v.getName();
+ }
+ return list;
+ }
+
+ /**
+ * Returns true if the synthesizer is generating speech, or have utterance waiting in the queue.
+ */
+ public boolean isSpeaking() {
+ return speaking;
+ }
+
+ /**
+ * Returns true if the synthesizer is in a paused state.
+ */
+ public boolean isPaused() {
+ return paused;
+ }
+}
diff --git a/platform/android/java/lib/src/org/pandemoniumengine/pandemonium/tts/PandemoniumUtterance.java b/platform/android/java/lib/src/org/pandemoniumengine/pandemonium/tts/PandemoniumUtterance.java
new file mode 100644
index 000000000..8b710cae9
--- /dev/null
+++ b/platform/android/java/lib/src/org/pandemoniumengine/pandemonium/tts/PandemoniumUtterance.java
@@ -0,0 +1,55 @@
+/*************************************************************************/
+/* PandemoniumUtterance.java */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+package org.pandemoniumengine.pandemonium.tts;
+
+/**
+ * A speech request for PandemoniumTTS.
+ */
+class PandemoniumUtterance {
+ final String text;
+ final String voice;
+ final int volume;
+ final float pitch;
+ final float rate;
+ final int id;
+
+ int offset = -1;
+ int start = 0;
+
+ PandemoniumUtterance(String text, String voice, int volume, float pitch, float rate, int id) {
+ this.text = text;
+ this.voice = voice;
+ this.volume = volume;
+ this.pitch = pitch;
+ this.rate = rate;
+ this.id = id;
+ }
+}
diff --git a/platform/android/java_pandemonium_lib_jni.cpp b/platform/android/java_pandemonium_lib_jni.cpp
index 79cd52d88..238558d01 100644
--- a/platform/android/java_pandemonium_lib_jni.cpp
+++ b/platform/android/java_pandemonium_lib_jni.cpp
@@ -49,6 +49,7 @@
#include "os_android.h"
#include "string_android.h"
#include "thread_jandroid.h"
+#include "tts_android.h"
#ifdef TOOLS_ENABLED
#include "editor/editor_settings.h"
@@ -178,6 +179,7 @@ JNIEXPORT void JNICALL Java_org_pandemoniumengine_pandemonium_PandemoniumLib_ini
DirAccessJAndroid::setup(p_directory_access_handler);
FileAccessFilesystemJAndroid::setup(p_file_access_handler);
NetSocketAndroid::setup(p_net_utils);
+ TTS_Android::setup(godot_java->get_member_object("tts", "Lorg/pandemoniumengine/pandemonium/tts/PandemoniumTTS;", env));
os_android = new OS_Android(pandemonium_java, pandemonium_io_java, p_use_apk_expansion);
@@ -264,6 +266,10 @@ JNIEXPORT void JNICALL Java_org_pandemoniumengine_pandemonium_PandemoniumLib_bac
}
}
+JNIEXPORT void JNICALL Java_org_pandemoniumengine_pandemonium_PandemoniumLib_ttsCallback(JNIEnv *env, jclass clazz, jint event, jint id, jint pos) {
+ TTS_Android::_java_utterance_callback(event, id, pos);
+}
+
JNIEXPORT void JNICALL Java_org_pandemoniumengine_pandemonium_PandemoniumLib_step(JNIEnv *env, jclass clazz) {
if (step.get() == -1)
return;
diff --git a/platform/android/java_pandemonium_lib_jni.h b/platform/android/java_pandemonium_lib_jni.h
index e67e9dbbc..bb576c9fa 100644
--- a/platform/android/java_pandemonium_lib_jni.h
+++ b/platform/android/java_pandemonium_lib_jni.h
@@ -44,6 +44,7 @@ JNIEXPORT void JNICALL Java_org_pandemoniumengine_pandemonium_PandemoniumLib_ond
JNIEXPORT jboolean JNICALL Java_org_pandemoniumengine_pandemonium_PandemoniumLib_setup(JNIEnv *env, jclass clazz, jobjectArray p_cmdline);
JNIEXPORT void JNICALL Java_org_pandemoniumengine_pandemonium_PandemoniumLib_resize(JNIEnv *env, jclass clazz, jint width, jint height);
JNIEXPORT void JNICALL Java_org_pandemoniumengine_pandemonium_PandemoniumLib_newcontext(JNIEnv *env, jclass clazz);
+JNIEXPORT void JNICALL Java_org_pandemoniumengine_pandemonium_PandemoniumLib_ttsCallback(JNIEnv *env, jclass clazz, jint event, jint id, jint pos);
JNIEXPORT void JNICALL Java_org_pandemoniumengine_pandemonium_PandemoniumLib_step(JNIEnv *env, jclass clazz);
JNIEXPORT void JNICALL Java_org_pandemoniumengine_pandemonium_PandemoniumLib_back(JNIEnv *env, jclass clazz);
JNIEXPORT void JNICALL Java_org_pandemoniumengine_pandemonium_PandemoniumLib_dispatchMouseEvent(JNIEnv *env, jclass clazz, jint p_event_type, jint p_button_mask, jfloat p_x, jfloat p_y, jfloat p_delta_x, jfloat p_delta_y, jboolean p_double_click, jboolean p_source_mouse_relative);
diff --git a/platform/android/os_android.cpp b/platform/android/os_android.cpp
index b386c69d2..9683ebf7c 100644
--- a/platform/android/os_android.cpp
+++ b/platform/android/os_android.cpp
@@ -55,6 +55,7 @@
#include "java_pandemonium_io_wrapper.h"
#include "java_pandemonium_wrapper.h"
+#include "tts_android.h"
const char *OS_Android::ANDROID_EXEC_PATH = "apk";
static const int DEFAULT_WINDOW_WIDTH = 800;
@@ -86,6 +87,34 @@ public:
virtual ~AndroidLogger() {}
};
+bool OS_Android::tts_is_speaking() const {
+ return TTS_Android::is_speaking();
+}
+
+bool OS_Android::tts_is_paused() const {
+ return TTS_Android::is_paused();
+}
+
+Array OS_Android::tts_get_voices() const {
+ return TTS_Android::get_voices();
+}
+
+void OS_Android::tts_speak(const String &p_text, const String &p_voice, int p_volume, float p_pitch, float p_rate, int p_utterance_id, bool p_interrupt) {
+ TTS_Android::speak(p_text, p_voice, p_volume, p_pitch, p_rate, p_utterance_id, p_interrupt);
+}
+
+void OS_Android::tts_pause() {
+ TTS_Android::pause();
+}
+
+void OS_Android::tts_resume() {
+ TTS_Android::resume();
+}
+
+void OS_Android::tts_stop() {
+ TTS_Android::stop();
+}
+
int OS_Android::get_video_driver_count() const {
return 2;
}
diff --git a/platform/android/os_android.h b/platform/android/os_android.h
index b6e692dec..47ba124cd 100644
--- a/platform/android/os_android.h
+++ b/platform/android/os_android.h
@@ -98,6 +98,15 @@ class OS_Android : public OS_Unix {
public:
static const char *ANDROID_EXEC_PATH;
+ virtual bool tts_is_speaking() const;
+ virtual bool tts_is_paused() const;
+ virtual Array tts_get_voices() const;
+
+ virtual void tts_speak(const String &p_text, const String &p_voice, int p_volume = 50, float p_pitch = 1.f, float p_rate = 1.f, int p_utterance_id = 0, bool p_interrupt = false);
+ virtual void tts_pause();
+ virtual void tts_resume();
+ virtual void tts_stop();
+
// functions used by main to initialize/deinitialize the OS
virtual int get_video_driver_count() const;
virtual const char *get_video_driver_name(int p_driver) const;
diff --git a/platform/android/tts_android.cpp b/platform/android/tts_android.cpp
new file mode 100644
index 000000000..a659c29c9
--- /dev/null
+++ b/platform/android/tts_android.cpp
@@ -0,0 +1,240 @@
+/*************************************************************************/
+/* tts_android.cpp */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#include "tts_android.h"
+
+#include "java_godot_wrapper.h"
+#include "os_android.h"
+#include "string_android.h"
+#include "thread_jandroid.h"
+
+jobject TTS_Android::tts = 0;
+jclass TTS_Android::cls = 0;
+
+jmethodID TTS_Android::_is_speaking = 0;
+jmethodID TTS_Android::_is_paused = 0;
+jmethodID TTS_Android::_get_voices = 0;
+jmethodID TTS_Android::_speak = 0;
+jmethodID TTS_Android::_pause_speaking = 0;
+jmethodID TTS_Android::_resume_speaking = 0;
+jmethodID TTS_Android::_stop_speaking = 0;
+
+HashMap> TTS_Android::ids;
+
+Vector TTS_Android::str_to_utf16(const String &p_string) {
+ int l = p_string.length();
+ if (!l) {
+ return Vector();
+ }
+
+ const CharType *d = &p_string[0];
+ int fl = 0;
+ for (int i = 0; i < l; i++) {
+ uint32_t c = d[i];
+ if (c <= 0xffff) { // 16 bits.
+ fl += 1;
+ } else if (c <= 0x10ffff) { // 32 bits.
+ fl += 2;
+ } else {
+ print_error("Unicode parsing error: Invalid unicode codepoint " + String::num_int64(c, 16) + ".");
+ return Vector();
+ }
+ if (c >= 0xd800 && c <= 0xdfff) {
+ print_error("Unicode parsing error: Invalid unicode codepoint " + String::num_int64(c, 16) + ".");
+ return Vector();
+ }
+ }
+
+ Vector utf16s;
+ if (fl == 0) {
+ return utf16s;
+ }
+
+ utf16s.resize(fl + 1);
+ uint16_t *cdst = (uint16_t *)utf16s.ptrw();
+
+#define APPEND_CHAR(m_c) *(cdst++) = m_c
+
+ for (int i = 0; i < l; i++) {
+ uint32_t c = d[i];
+
+ if (c <= 0xffff) { // 16 bits.
+ APPEND_CHAR(c);
+ } else { // 32 bits.
+ APPEND_CHAR(uint32_t((c >> 10) + 0xd7c0)); // lead surrogate.
+ APPEND_CHAR(uint32_t((c & 0x3ff) | 0xdc00)); // trail surrogate.
+ }
+ }
+#undef APPEND_CHAR
+ *cdst = 0; //trailing zero
+
+ return utf16s;
+}
+
+void TTS_Android::setup(jobject p_tts) {
+ JNIEnv *env = get_jni_env();
+
+ tts = env->NewGlobalRef(p_tts);
+
+ jclass c = env->GetObjectClass(tts);
+ cls = (jclass)env->NewGlobalRef(c);
+
+ _is_speaking = env->GetMethodID(cls, "isSpeaking", "()Z");
+ _is_paused = env->GetMethodID(cls, "isPaused", "()Z");
+ _get_voices = env->GetMethodID(cls, "getVoices", "()[Ljava/lang/String;");
+ _speak = env->GetMethodID(cls, "speak", "(Ljava/lang/String;Ljava/lang/String;IFFIZ)V");
+ _pause_speaking = env->GetMethodID(cls, "pauseSpeaking", "()V");
+ _resume_speaking = env->GetMethodID(cls, "resumeSpeaking", "()V");
+ _stop_speaking = env->GetMethodID(cls, "stopSpeaking", "()V");
+}
+
+void TTS_Android::_java_utterance_callback(int p_event, int p_id, int p_pos) {
+ if (ids.has(p_id)) {
+ int pos = 0;
+ if ((OS::TTSUtteranceEvent)p_event == OS::TTS_UTTERANCE_BOUNDARY) {
+ // Convert position from UTF-16 to UTF-32.
+ const Vector &string = ids[p_id];
+ for (int i = 0; i < MIN(p_pos, string.size() - 1); i++) {
+ char16_t c = string[i];
+ if ((c & 0xfffffc00) == 0xd800) {
+ i++;
+ }
+ pos++;
+ }
+ } else if ((OS::TTSUtteranceEvent)p_event != OS::TTS_UTTERANCE_STARTED) {
+ ids.erase(p_id);
+ }
+ OS::get_singleton()->tts_post_utterance_event((OS::TTSUtteranceEvent)p_event, p_id, pos);
+ }
+}
+
+bool TTS_Android::is_speaking() {
+ if (_is_speaking) {
+ JNIEnv *env = get_jni_env();
+
+ ERR_FAIL_COND_V(env == nullptr, false);
+ return env->CallBooleanMethod(tts, _is_speaking);
+ } else {
+ return false;
+ }
+}
+
+bool TTS_Android::is_paused() {
+ if (_is_paused) {
+ JNIEnv *env = get_jni_env();
+
+ ERR_FAIL_COND_V(env == nullptr, false);
+ return env->CallBooleanMethod(tts, _is_paused);
+ } else {
+ return false;
+ }
+}
+
+Array TTS_Android::get_voices() {
+ Array list;
+ if (_get_voices) {
+ JNIEnv *env = get_jni_env();
+ ERR_FAIL_COND_V(env == nullptr, list);
+
+ jobject voices_object = env->CallObjectMethod(tts, _get_voices);
+ jobjectArray *arr = reinterpret_cast(&voices_object);
+
+ jsize len = env->GetArrayLength(*arr);
+ for (int i = 0; i < len; i++) {
+ jstring jStr = (jstring)env->GetObjectArrayElement(*arr, i);
+ String str = jstring_to_string(jStr, env);
+ Vector tokens = str.split(";", true, 2);
+ if (tokens.size() == 2) {
+ Dictionary voice_d;
+ voice_d["name"] = tokens[1];
+ voice_d["id"] = tokens[1];
+ voice_d["language"] = tokens[0];
+ list.push_back(voice_d);
+ }
+ env->DeleteLocalRef(jStr);
+ }
+ }
+ return list;
+}
+
+void TTS_Android::speak(const String &p_text, const String &p_voice, int p_volume, float p_pitch, float p_rate, int p_utterance_id, bool p_interrupt) {
+ if (p_interrupt) {
+ stop();
+ }
+
+ if (p_text.empty()) {
+ OS::get_singleton()->tts_post_utterance_event(OS::TTS_UTTERANCE_CANCELED, p_utterance_id);
+ return;
+ }
+
+ ids[p_utterance_id] = str_to_utf16(p_text);
+
+ if (_speak) {
+ JNIEnv *env = get_jni_env();
+ ERR_FAIL_COND(env == nullptr);
+
+ jstring jStrT = env->NewStringUTF(p_text.utf8().get_data());
+ jstring jStrV = env->NewStringUTF(p_voice.utf8().get_data());
+ env->CallVoidMethod(tts, _speak, jStrT, jStrV, CLAMP(p_volume, 0, 100), CLAMP(p_pitch, 0.f, 2.f), CLAMP(p_rate, 0.1f, 10.f), p_utterance_id, p_interrupt);
+ }
+}
+
+void TTS_Android::pause() {
+ if (_pause_speaking) {
+ JNIEnv *env = get_jni_env();
+
+ ERR_FAIL_COND(env == nullptr);
+ env->CallVoidMethod(tts, _pause_speaking);
+ }
+}
+
+void TTS_Android::resume() {
+ if (_resume_speaking) {
+ JNIEnv *env = get_jni_env();
+
+ ERR_FAIL_COND(env == nullptr);
+ env->CallVoidMethod(tts, _resume_speaking);
+ }
+}
+
+void TTS_Android::stop() {
+ const int *k = NULL;
+ while ((k = ids.next(k))) {
+ OS::get_singleton()->tts_post_utterance_event(OS::TTS_UTTERANCE_CANCELED, *k);
+ }
+ ids.clear();
+
+ if (_stop_speaking) {
+ JNIEnv *env = get_jni_env();
+
+ ERR_FAIL_COND(env == nullptr);
+ env->CallVoidMethod(tts, _stop_speaking);
+ }
+}
diff --git a/platform/android/tts_android.h b/platform/android/tts_android.h
new file mode 100644
index 000000000..09d48c89b
--- /dev/null
+++ b/platform/android/tts_android.h
@@ -0,0 +1,69 @@
+/*************************************************************************/
+/* tts_android.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TTS_ANDROID_H
+#define TTS_ANDROID_H
+
+#include "core/array.h"
+#include "core/os/os.h"
+#include "core/ustring.h"
+
+#include
+
+class TTS_Android {
+ static jobject tts;
+ static jclass cls;
+
+ static jmethodID _is_speaking;
+ static jmethodID _is_paused;
+ static jmethodID _get_voices;
+ static jmethodID _speak;
+ static jmethodID _pause_speaking;
+ static jmethodID _resume_speaking;
+ static jmethodID _stop_speaking;
+
+ static HashMap> ids;
+
+ static Vector str_to_utf16(const String &p_string);
+
+public:
+ static void setup(jobject p_tts);
+ static void _java_utterance_callback(int p_event, int p_id, int p_pos);
+
+ static bool is_speaking();
+ static bool is_paused();
+ static Array get_voices();
+ static void speak(const String &p_text, const String &p_voice, int p_volume, float p_pitch, float p_rate, int p_utterance_id, bool p_interrupt);
+ static void pause();
+ static void resume();
+ static void stop();
+};
+
+#endif // TTS_ANDROID_H
diff --git a/platform/iphone/SCsub b/platform/iphone/SCsub
index 6b4624e83..c23e5ff03 100644
--- a/platform/iphone/SCsub
+++ b/platform/iphone/SCsub
@@ -11,6 +11,7 @@ iphone_lib = [
"ios.mm",
"joypad_iphone.mm",
"pandemonium_view.mm",
+ "tts_ios.mm",
"display_layer.mm",
"pandemonium_app_delegate.m",
"pandemonium_view_renderer.mm",
diff --git a/platform/iphone/os_iphone.h b/platform/iphone/os_iphone.h
index cd55d7a01..4e1ad9c60 100644
--- a/platform/iphone/os_iphone.h
+++ b/platform/iphone/os_iphone.h
@@ -56,6 +56,8 @@ private:
iOS *ios;
+ id tts = nullptr;
+
JoypadIPhone *joypad_iphone;
MainLoop *main_loop;
@@ -104,6 +106,15 @@ public:
void start();
+ virtual bool tts_is_speaking() const;
+ virtual bool tts_is_paused() const;
+ virtual Array tts_get_voices() const;
+
+ virtual void tts_speak(const String &p_text, const String &p_voice, int p_volume = 50, float p_pitch = 1.f, float p_rate = 1.f, int p_utterance_id = 0, bool p_interrupt = false);
+ virtual void tts_pause();
+ virtual void tts_resume();
+ virtual void tts_stop();
+
virtual Error open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path = false);
virtual Error close_dynamic_library(void *p_library_handle);
virtual Error get_dynamic_library_symbol_handle(void *p_library_handle, const String p_name, void *&p_symbol_handle, bool p_optional = false);
diff --git a/platform/iphone/os_iphone.mm b/platform/iphone/os_iphone.mm
index faf0284f7..92194f5fd 100644
--- a/platform/iphone/os_iphone.mm
+++ b/platform/iphone/os_iphone.mm
@@ -55,6 +55,8 @@
#import "pandemonium_view.h"
#import "view_controller.h"
+#include "tts_ios.h"
+
#import
#include
#include
@@ -92,6 +94,41 @@ OSIPhone *OSIPhone::get_singleton() {
return (OSIPhone *)OS::get_singleton();
};
+bool OSIPhone::tts_is_speaking() const {
+ ERR_FAIL_COND_V(!tts, false);
+ return [tts isSpeaking];
+}
+
+bool OSIPhone::tts_is_paused() const {
+ ERR_FAIL_COND_V(!tts, false);
+ return [tts isPaused];
+}
+
+Array OSIPhone::tts_get_voices() const {
+ ERR_FAIL_COND_V(!tts, Array());
+ return [tts getVoices];
+}
+
+void OSIPhone::tts_speak(const String &p_text, const String &p_voice, int p_volume, float p_pitch, float p_rate, int p_utterance_id, bool p_interrupt) {
+ ERR_FAIL_COND(!tts);
+ [tts speak:p_text voice:p_voice volume:p_volume pitch:p_pitch rate:p_rate utterance_id:p_utterance_id interrupt:p_interrupt];
+}
+
+void OSIPhone::tts_pause() {
+ ERR_FAIL_COND(!tts);
+ [tts pauseSpeaking];
+}
+
+void OSIPhone::tts_resume() {
+ ERR_FAIL_COND(!tts);
+ [tts resumeSpeaking];
+}
+
+void OSIPhone::tts_stop() {
+ ERR_FAIL_COND(!tts);
+ [tts stopSpeaking];
+}
+
void OSIPhone::set_data_dir(String p_dir) {
DirAccess *da = DirAccess::open(p_dir);
@@ -187,6 +224,9 @@ Error OSIPhone::initialize(const VideoMode &p_desired, int p_video_driver, int p
rendering_server = memnew(RenderingServerWrapMT(rendering_server, false));
}
+ // Init TTS
+ tts = [[TTS_IOS alloc] init];
+
rendering_server->init();
//rendering_server->cursor_set_visible(false, 0);
diff --git a/platform/iphone/tts_ios.h b/platform/iphone/tts_ios.h
new file mode 100644
index 000000000..2ce98db14
--- /dev/null
+++ b/platform/iphone/tts_ios.h
@@ -0,0 +1,63 @@
+/*************************************************************************/
+/* tts_ios.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TTS_IOS_H
+#define TTS_IOS_H
+
+#if __has_include()
+#import
+#else
+#import
+#endif
+
+#include "core/variant/array.h"
+#include "core/containers/list.h"
+#include "core/containers/rb_map.h"
+#include "core/os/os.h"
+#include "core/string/ustring.h"
+
+@interface TTS_IOS : NSObject {
+ bool speaking;
+ RBMap ids;
+
+ AVSpeechSynthesizer *av_synth;
+ List queue;
+}
+
+- (void)pauseSpeaking;
+- (void)resumeSpeaking;
+- (void)stopSpeaking;
+- (bool)isSpeaking;
+- (bool)isPaused;
+- (void)speak:(const String &)text voice:(const String &)voice volume:(int)volume pitch:(float)pitch rate:(float)rate utterance_id:(int)utterance_id interrupt:(bool)interrupt;
+- (Array)getVoices;
+@end
+
+#endif // TTS_IOS_H
diff --git a/platform/iphone/tts_ios.mm b/platform/iphone/tts_ios.mm
new file mode 100644
index 000000000..e867094d2
--- /dev/null
+++ b/platform/iphone/tts_ios.mm
@@ -0,0 +1,165 @@
+/*************************************************************************/
+/* tts_ios.mm */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#include "tts_ios.h"
+
+@implementation TTS_IOS
+
+- (id)init {
+ self = [super init];
+ self->speaking = false;
+ self->av_synth = [[AVSpeechSynthesizer alloc] init];
+ [self->av_synth setDelegate:self];
+ print_verbose("Text-to-Speech: AVSpeechSynthesizer initialized.");
+ return self;
+}
+
+- (void)speechSynthesizer:(AVSpeechSynthesizer *)av_synth willSpeakRangeOfSpeechString:(NSRange)characterRange utterance:(AVSpeechUtterance *)utterance {
+ NSString *string = [utterance speechString];
+
+ // Convert from UTF-16 to UTF-32 position.
+ int pos = 0;
+ for (NSUInteger i = 0; i < MIN(characterRange.location, string.length); i++) {
+ unichar c = [string characterAtIndex:i];
+ if ((c & 0xfffffc00) == 0xd800) {
+ i++;
+ }
+ pos++;
+ }
+
+ OS::get_singleton()->tts_post_utterance_event(OS::TTS_UTTERANCE_BOUNDARY, ids[utterance], pos);
+}
+
+- (void)speechSynthesizer:(AVSpeechSynthesizer *)av_synth didCancelSpeechUtterance:(AVSpeechUtterance *)utterance {
+ OS::get_singleton()->tts_post_utterance_event(OS::TTS_UTTERANCE_CANCELED, ids[utterance]);
+ ids.erase(utterance);
+ speaking = false;
+ [self update];
+}
+
+- (void)speechSynthesizer:(AVSpeechSynthesizer *)av_synth didFinishSpeechUtterance:(AVSpeechUtterance *)utterance {
+ OS::get_singleton()->tts_post_utterance_event(OS::TTS_UTTERANCE_ENDED, ids[utterance]);
+ ids.erase(utterance);
+ speaking = false;
+ [self update];
+}
+
+- (void)update {
+ if (!speaking && queue.size() > 0) {
+ OS::TTSUtterance &message = queue.front()->get();
+
+ AVSpeechUtterance *new_utterance = [[AVSpeechUtterance alloc] initWithString:[NSString stringWithUTF8String:message.text.utf8().get_data()]];
+ [new_utterance setVoice:[AVSpeechSynthesisVoice voiceWithIdentifier:[NSString stringWithUTF8String:message.voice.utf8().get_data()]]];
+ if (message.rate > 1.f) {
+ [new_utterance setRate:Math::range_lerp(message.rate, 1.f, 10.f, AVSpeechUtteranceDefaultSpeechRate, AVSpeechUtteranceMaximumSpeechRate)];
+ } else if (message.rate < 1.f) {
+ [new_utterance setRate:Math::range_lerp(message.rate, 0.1f, 1.f, AVSpeechUtteranceMinimumSpeechRate, AVSpeechUtteranceDefaultSpeechRate)];
+ }
+ [new_utterance setPitchMultiplier:message.pitch];
+ [new_utterance setVolume:(Math::range_lerp(message.volume, 0.f, 100.f, 0.f, 1.f))];
+
+ ids[new_utterance] = message.id;
+ [av_synth speakUtterance:new_utterance];
+
+ queue.pop_front();
+
+ OS::get_singleton()->tts_post_utterance_event(OS::TTS_UTTERANCE_STARTED, message.id);
+ speaking = true;
+ }
+}
+
+- (void)pauseSpeaking {
+ [av_synth pauseSpeakingAtBoundary:AVSpeechBoundaryImmediate];
+}
+
+- (void)resumeSpeaking {
+ [av_synth continueSpeaking];
+}
+
+- (void)stopSpeaking {
+ for (List::Element *E = queue.front(); E; E = E->next()) {
+ OS::TTSUtterance &message = E->get();
+ OS::get_singleton()->tts_post_utterance_event(OS::TTS_UTTERANCE_CANCELED, message.id);
+ }
+ queue.clear();
+ [av_synth stopSpeakingAtBoundary:AVSpeechBoundaryImmediate];
+ speaking = false;
+}
+
+- (bool)isSpeaking {
+ return speaking || (queue.size() > 0);
+}
+
+- (bool)isPaused {
+ return [av_synth isPaused];
+}
+
+- (void)speak:(const String &)text voice:(const String &)voice volume:(int)volume pitch:(float)pitch rate:(float)rate utterance_id:(int)utterance_id interrupt:(bool)interrupt {
+ if (interrupt) {
+ [self stopSpeaking];
+ }
+
+ if (text.empty()) {
+ OS::get_singleton()->tts_post_utterance_event(OS::TTS_UTTERANCE_CANCELED, utterance_id);
+ return;
+ }
+
+ OS::TTSUtterance message;
+ message.text = text;
+ message.voice = voice;
+ message.volume = CLAMP(volume, 0, 100);
+ message.pitch = CLAMP(pitch, 0.f, 2.f);
+ message.rate = CLAMP(rate, 0.1f, 10.f);
+ message.id = utterance_id;
+ queue.push_back(message);
+
+ if ([self isPaused]) {
+ [self resumeSpeaking];
+ } else {
+ [self update];
+ }
+}
+
+- (Array)getVoices {
+ Array list;
+ for (AVSpeechSynthesisVoice *voice in [AVSpeechSynthesisVoice speechVoices]) {
+ NSString *voiceIdentifierString = [voice identifier];
+ NSString *voiceLocaleIdentifier = [voice language];
+ NSString *voiceName = [voice name];
+ Dictionary voice_d;
+ voice_d["name"] = String::utf8([voiceName UTF8String]);
+ voice_d["id"] = String::utf8([voiceIdentifierString UTF8String]);
+ voice_d["language"] = String::utf8([voiceLocaleIdentifier UTF8String]);
+ list.push_back(voice_d);
+ }
+ return list;
+}
+
+@end
diff --git a/platform/javascript/js/libs/library_godot_display.js b/platform/javascript/js/libs/library_godot_display.js
new file mode 100644
index 000000000..c4a5b0b28
--- /dev/null
+++ b/platform/javascript/js/libs/library_godot_display.js
@@ -0,0 +1,741 @@
+/*************************************************************************/
+/* library_godot_display.js */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+const GodotDisplayVK = {
+
+ $GodotDisplayVK__deps: ['$GodotRuntime', '$GodotConfig', '$GodotEventListeners'],
+ $GodotDisplayVK__postset: 'GodotOS.atexit(function(resolve, reject) { GodotDisplayVK.clear(); resolve(); });',
+ $GodotDisplayVK: {
+ textinput: null,
+ textarea: null,
+
+ available: function () {
+ return GodotConfig.virtual_keyboard && 'ontouchstart' in window;
+ },
+
+ init: function (input_cb) {
+ function create(what) {
+ const elem = document.createElement(what);
+ elem.style.display = 'none';
+ elem.style.position = 'absolute';
+ elem.style.zIndex = '-1';
+ elem.style.background = 'transparent';
+ elem.style.padding = '0px';
+ elem.style.margin = '0px';
+ elem.style.overflow = 'hidden';
+ elem.style.width = '0px';
+ elem.style.height = '0px';
+ elem.style.border = '0px';
+ elem.style.outline = 'none';
+ elem.readonly = true;
+ elem.disabled = true;
+ GodotEventListeners.add(elem, 'input', function (evt) {
+ const c_str = GodotRuntime.allocString(elem.value);
+ input_cb(c_str, elem.selectionEnd);
+ GodotRuntime.free(c_str);
+ }, false);
+ GodotEventListeners.add(elem, 'blur', function (evt) {
+ elem.style.display = 'none';
+ elem.readonly = true;
+ elem.disabled = true;
+ }, false);
+ GodotConfig.canvas.insertAdjacentElement('beforebegin', elem);
+ return elem;
+ }
+ GodotDisplayVK.textinput = create('input');
+ GodotDisplayVK.textarea = create('textarea');
+ GodotDisplayVK.updateSize();
+ },
+ show: function (text, multiline, start, end) {
+ if (!GodotDisplayVK.textinput || !GodotDisplayVK.textarea) {
+ return;
+ }
+ if (GodotDisplayVK.textinput.style.display !== '' || GodotDisplayVK.textarea.style.display !== '') {
+ GodotDisplayVK.hide();
+ }
+ GodotDisplayVK.updateSize();
+ const elem = multiline ? GodotDisplayVK.textarea : GodotDisplayVK.textinput;
+ elem.readonly = false;
+ elem.disabled = false;
+ elem.value = text;
+ elem.style.display = 'block';
+ elem.focus();
+ elem.setSelectionRange(start, end);
+ },
+ hide: function () {
+ if (!GodotDisplayVK.textinput || !GodotDisplayVK.textarea) {
+ return;
+ }
+ [GodotDisplayVK.textinput, GodotDisplayVK.textarea].forEach(function (elem) {
+ elem.blur();
+ elem.style.display = 'none';
+ elem.value = '';
+ });
+ },
+ updateSize: function () {
+ if (!GodotDisplayVK.textinput || !GodotDisplayVK.textarea) {
+ return;
+ }
+ const rect = GodotConfig.canvas.getBoundingClientRect();
+ function update(elem) {
+ elem.style.left = `${rect.left}px`;
+ elem.style.top = `${rect.top}px`;
+ elem.style.width = `${rect.width}px`;
+ elem.style.height = `${rect.height}px`;
+ }
+ update(GodotDisplayVK.textinput);
+ update(GodotDisplayVK.textarea);
+ },
+ clear: function () {
+ if (GodotDisplayVK.textinput) {
+ GodotDisplayVK.textinput.remove();
+ GodotDisplayVK.textinput = null;
+ }
+ if (GodotDisplayVK.textarea) {
+ GodotDisplayVK.textarea.remove();
+ GodotDisplayVK.textarea = null;
+ }
+ },
+ },
+};
+mergeInto(LibraryManager.library, GodotDisplayVK);
+
+/*
+ * Display server cursor helper.
+ * Keeps track of cursor status and custom shapes.
+ */
+const GodotDisplayCursor = {
+ $GodotDisplayCursor__deps: ['$GodotOS', '$GodotConfig'],
+ $GodotDisplayCursor__postset: 'GodotOS.atexit(function(resolve, reject) { GodotDisplayCursor.clear(); resolve(); });',
+ $GodotDisplayCursor: {
+ shape: 'auto',
+ visible: true,
+ cursors: {},
+ set_style: function (style) {
+ GodotConfig.canvas.style.cursor = style;
+ },
+ set_shape: function (shape) {
+ GodotDisplayCursor.shape = shape;
+ let css = shape;
+ if (shape in GodotDisplayCursor.cursors) {
+ const c = GodotDisplayCursor.cursors[shape];
+ css = `url("${c.url}") ${c.x} ${c.y}, auto`;
+ }
+ if (GodotDisplayCursor.visible) {
+ GodotDisplayCursor.set_style(css);
+ }
+ },
+ clear: function () {
+ GodotDisplayCursor.set_style('');
+ GodotDisplayCursor.shape = 'auto';
+ GodotDisplayCursor.visible = true;
+ Object.keys(GodotDisplayCursor.cursors).forEach(function (key) {
+ URL.revokeObjectURL(GodotDisplayCursor.cursors[key]);
+ delete GodotDisplayCursor.cursors[key];
+ });
+ },
+ lockPointer: function () {
+ const canvas = GodotConfig.canvas;
+ if (canvas.requestPointerLock) {
+ canvas.requestPointerLock();
+ }
+ },
+ releasePointer: function () {
+ if (document.exitPointerLock) {
+ document.exitPointerLock();
+ }
+ },
+ isPointerLocked: function () {
+ return document.pointerLockElement === GodotConfig.canvas;
+ },
+ },
+};
+mergeInto(LibraryManager.library, GodotDisplayCursor);
+
+const GodotDisplayScreen = {
+ $GodotDisplayScreen__deps: ['$GodotConfig', '$GodotOS', '$GL', 'emscripten_webgl_get_current_context'],
+ $GodotDisplayScreen: {
+ desired_size: [0, 0],
+ hidpi: true,
+ getPixelRatio: function () {
+ return GodotDisplayScreen.hidpi ? window.devicePixelRatio || 1 : 1;
+ },
+ isFullscreen: function () {
+ const elem = document.fullscreenElement || document.mozFullscreenElement
+ || document.webkitFullscreenElement || document.msFullscreenElement;
+ if (elem) {
+ return elem === GodotConfig.canvas;
+ }
+ // But maybe knowing the element is not supported.
+ return document.fullscreen || document.mozFullScreen
+ || document.webkitIsFullscreen;
+ },
+ hasFullscreen: function () {
+ return document.fullscreenEnabled || document.mozFullScreenEnabled
+ || document.webkitFullscreenEnabled;
+ },
+ requestFullscreen: function () {
+ if (!GodotDisplayScreen.hasFullscreen()) {
+ return 1;
+ }
+ const canvas = GodotConfig.canvas;
+ try {
+ const promise = (canvas.requestFullscreen || canvas.msRequestFullscreen
+ || canvas.mozRequestFullScreen || canvas.mozRequestFullscreen
+ || canvas.webkitRequestFullscreen
+ ).call(canvas);
+ // Some browsers (Safari) return undefined.
+ // For the standard ones, we need to catch it.
+ if (promise) {
+ promise.catch(function () {
+ // nothing to do.
+ });
+ }
+ } catch (e) {
+ return 1;
+ }
+ return 0;
+ },
+ exitFullscreen: function () {
+ if (!GodotDisplayScreen.isFullscreen()) {
+ return 0;
+ }
+ try {
+ const promise = document.exitFullscreen();
+ if (promise) {
+ promise.catch(function () {
+ // nothing to do.
+ });
+ }
+ } catch (e) {
+ return 1;
+ }
+ return 0;
+ },
+ _updateGL: function () {
+ const gl_context_handle = _emscripten_webgl_get_current_context(); // eslint-disable-line no-undef
+ const gl = GL.getContext(gl_context_handle);
+ if (gl) {
+ GL.resizeOffscreenFramebuffer(gl);
+ }
+ },
+ updateSize: function () {
+ const isFullscreen = GodotDisplayScreen.isFullscreen();
+ const wantsFullWindow = GodotConfig.canvas_resize_policy === 2;
+ const noResize = GodotConfig.canvas_resize_policy === 0;
+ const wwidth = GodotDisplayScreen.desired_size[0];
+ const wheight = GodotDisplayScreen.desired_size[1];
+ const canvas = GodotConfig.canvas;
+ let width = wwidth;
+ let height = wheight;
+ if (noResize) {
+ // Don't resize canvas, just update GL if needed.
+ if (canvas.width !== width || canvas.height !== height) {
+ GodotDisplayScreen.desired_size = [canvas.width, canvas.height];
+ GodotDisplayScreen._updateGL();
+ return 1;
+ }
+ return 0;
+ }
+ const scale = GodotDisplayScreen.getPixelRatio();
+ if (isFullscreen || wantsFullWindow) {
+ // We need to match screen size.
+ width = window.innerWidth * scale;
+ height = window.innerHeight * scale;
+ }
+ const csw = `${width / scale}px`;
+ const csh = `${height / scale}px`;
+ if (canvas.style.width !== csw || canvas.style.height !== csh || canvas.width !== width || canvas.height !== height) {
+ // Size doesn't match.
+ // Resize canvas, set correct CSS pixel size, update GL.
+ canvas.width = width;
+ canvas.height = height;
+ canvas.style.width = csw;
+ canvas.style.height = csh;
+ GodotDisplayScreen._updateGL();
+ return 1;
+ }
+ return 0;
+ },
+ },
+};
+mergeInto(LibraryManager.library, GodotDisplayScreen);
+
+/**
+ * Display server interface.
+ *
+ * Exposes all the functions needed by OS implementation.
+ */
+const GodotDisplay = {
+ $GodotDisplay__deps: ['$GodotConfig', '$GodotRuntime', '$GodotDisplayCursor', '$GodotEventListeners', '$GodotDisplayScreen', '$GodotDisplayVK'],
+ $GodotDisplay: {
+ window_icon: '',
+ findDPI: function () {
+ function testDPI(dpi) {
+ return window.matchMedia(`(max-resolution: ${dpi}dpi)`).matches;
+ }
+ function bisect(low, high, func) {
+ const mid = parseInt(((high - low) / 2) + low, 10);
+ if (high - low <= 1) {
+ return func(high) ? high : low;
+ }
+ if (func(mid)) {
+ return bisect(low, mid, func);
+ }
+ return bisect(mid, high, func);
+ }
+ try {
+ const dpi = bisect(0, 800, testDPI);
+ return dpi >= 96 ? dpi : 96;
+ } catch (e) {
+ return 96;
+ }
+ },
+ },
+
+ // This is implemented as "glGetBufferSubData" in new emscripten versions.
+ // Since we have to support older (pre 2.0.17) emscripten versions, we add this wrapper function instead.
+ godot_js_display_glGetBufferSubData__sig: 'viiii',
+ godot_js_display_glGetBufferSubData__deps: ['$GL', 'emscripten_webgl_get_current_context'],
+ godot_js_display_glGetBufferSubData: function (target, offset, size, data) {
+ const gl_context_handle = _emscripten_webgl_get_current_context(); // eslint-disable-line no-undef
+ const gl = GL.getContext(gl_context_handle);
+ if (gl) {
+ gl.GLctx['getBufferSubData'](target, offset, HEAPU8, data, size);
+ }
+ },
+
+ godot_js_display_is_swap_ok_cancel__sig: 'i',
+ godot_js_display_is_swap_ok_cancel: function () {
+ const win = (['Windows', 'Win64', 'Win32', 'WinCE']);
+ const plat = navigator.platform || '';
+ if (win.indexOf(plat) !== -1) {
+ return 1;
+ }
+ return 0;
+ },
+
+ godot_js_tts_is_speaking__sig: 'i',
+ godot_js_tts_is_speaking: function () {
+ return window.speechSynthesis.speaking;
+ },
+
+ godot_js_tts_is_paused__sig: 'i',
+ godot_js_tts_is_paused: function () {
+ return window.speechSynthesis.paused;
+ },
+
+ godot_js_tts_get_voices__sig: 'vi',
+ godot_js_tts_get_voices: function (p_callback) {
+ const func = GodotRuntime.get_func(p_callback);
+ try {
+ const arr = [];
+ const voices = window.speechSynthesis.getVoices();
+ for (let i = 0; i < voices.length; i++) {
+ arr.push(`${voices[i].lang};${voices[i].name}`);
+ }
+ const c_ptr = GodotRuntime.allocStringArray(arr);
+ func(arr.length, c_ptr);
+ GodotRuntime.freeStringArray(c_ptr, arr.length);
+ } catch (e) {
+ // Fail graciously.
+ }
+ },
+
+ godot_js_tts_speak__sig: 'viiiffii',
+ godot_js_tts_speak: function (p_text, p_voice, p_volume, p_pitch, p_rate, p_utterance_id, p_callback) {
+ const func = GodotRuntime.get_func(p_callback);
+
+ function listener_end(evt) {
+ evt.currentTarget.cb(1 /*TTS_UTTERANCE_ENDED*/, evt.currentTarget.id, 0);
+ }
+
+ function listener_start(evt) {
+ evt.currentTarget.cb(0 /*TTS_UTTERANCE_STARTED*/, evt.currentTarget.id, 0);
+ }
+
+ function listener_error(evt) {
+ evt.currentTarget.cb(2 /*TTS_UTTERANCE_CANCELED*/, evt.currentTarget.id, 0);
+ }
+
+ function listener_bound(evt) {
+ evt.currentTarget.cb(3 /*TTS_UTTERANCE_BOUNDARY*/, evt.currentTarget.id, evt.charIndex);
+ }
+
+ const utterance = new SpeechSynthesisUtterance(GodotRuntime.parseString(p_text));
+ utterance.rate = p_rate;
+ utterance.pitch = p_pitch;
+ utterance.volume = p_volume / 100.0;
+ utterance.addEventListener('end', listener_end);
+ utterance.addEventListener('start', listener_start);
+ utterance.addEventListener('error', listener_error);
+ utterance.addEventListener('boundary', listener_bound);
+ utterance.id = p_utterance_id;
+ utterance.cb = func;
+ const voice = GodotRuntime.parseString(p_voice);
+ const voices = window.speechSynthesis.getVoices();
+ for (let i = 0; i < voices.length; i++) {
+ if (voices[i].name === voice) {
+ utterance.voice = voices[i];
+ break;
+ }
+ }
+ window.speechSynthesis.resume();
+ window.speechSynthesis.speak(utterance);
+ },
+
+ godot_js_tts_pause__sig: 'v',
+ godot_js_tts_pause: function () {
+ window.speechSynthesis.pause();
+ },
+
+ godot_js_tts_resume__sig: 'v',
+ godot_js_tts_resume: function () {
+ window.speechSynthesis.resume();
+ },
+
+ godot_js_tts_stop__sig: 'v',
+ godot_js_tts_stop: function () {
+ window.speechSynthesis.cancel();
+ window.speechSynthesis.resume();
+ },
+
+ godot_js_display_alert__sig: 'vi',
+ godot_js_display_alert: function (p_text) {
+ window.alert(GodotRuntime.parseString(p_text)); // eslint-disable-line no-alert
+ },
+
+ godot_js_display_screen_dpi_get__sig: 'i',
+ godot_js_display_screen_dpi_get: function () {
+ return GodotDisplay.findDPI();
+ },
+
+ godot_js_display_pixel_ratio_get__sig: 'f',
+ godot_js_display_pixel_ratio_get: function () {
+ return GodotDisplayScreen.getPixelRatio();
+ },
+
+ godot_js_display_fullscreen_request__sig: 'i',
+ godot_js_display_fullscreen_request: function () {
+ return GodotDisplayScreen.requestFullscreen();
+ },
+
+ godot_js_display_fullscreen_exit__sig: 'i',
+ godot_js_display_fullscreen_exit: function () {
+ return GodotDisplayScreen.exitFullscreen();
+ },
+
+ godot_js_display_desired_size_set__sig: 'vii',
+ godot_js_display_desired_size_set: function (width, height) {
+ GodotDisplayScreen.desired_size = [width, height];
+ GodotDisplayScreen.updateSize();
+ },
+
+ godot_js_display_size_update__sig: 'i',
+ godot_js_display_size_update: function () {
+ const updated = GodotDisplayScreen.updateSize();
+ if (updated) {
+ GodotDisplayVK.updateSize();
+ }
+ return updated;
+ },
+
+ godot_js_display_screen_size_get__sig: 'vii',
+ godot_js_display_screen_size_get: function (width, height) {
+ const scale = GodotDisplayScreen.getPixelRatio();
+ GodotRuntime.setHeapValue(width, window.screen.width * scale, 'i32');
+ GodotRuntime.setHeapValue(height, window.screen.height * scale, 'i32');
+ },
+
+ godot_js_display_window_size_get__sig: 'vii',
+ godot_js_display_window_size_get: function (p_width, p_height) {
+ GodotRuntime.setHeapValue(p_width, GodotConfig.canvas.width, 'i32');
+ GodotRuntime.setHeapValue(p_height, GodotConfig.canvas.height, 'i32');
+ },
+
+ godot_js_display_has_webgl__sig: 'ii',
+ godot_js_display_has_webgl: function (p_version) {
+ if (p_version !== 1 && p_version !== 2) {
+ return false;
+ }
+ try {
+ return !!document.createElement('canvas').getContext(p_version === 2 ? 'webgl2' : 'webgl');
+ } catch (e) { /* Not available */ }
+ return false;
+ },
+
+ /*
+ * Canvas
+ */
+ godot_js_display_canvas_focus__sig: 'v',
+ godot_js_display_canvas_focus: function () {
+ GodotConfig.canvas.focus();
+ },
+
+ godot_js_display_canvas_is_focused__sig: 'i',
+ godot_js_display_canvas_is_focused: function () {
+ return document.activeElement === GodotConfig.canvas;
+ },
+
+ /*
+ * Touchscreen
+ */
+ godot_js_display_touchscreen_is_available__sig: 'i',
+ godot_js_display_touchscreen_is_available: function () {
+ return 'ontouchstart' in window;
+ },
+
+ /*
+ * Clipboard
+ */
+ godot_js_display_clipboard_set__sig: 'ii',
+ godot_js_display_clipboard_set: function (p_text) {
+ const text = GodotRuntime.parseString(p_text);
+ if (!navigator.clipboard || !navigator.clipboard.writeText) {
+ return 1;
+ }
+ navigator.clipboard.writeText(text).catch(function (e) {
+ // Setting OS clipboard is only possible from an input callback.
+ GodotRuntime.error('Setting OS clipboard is only possible from an input callback for the HTML5 plafrom. Exception:', e);
+ });
+ return 0;
+ },
+
+ godot_js_display_clipboard_get__sig: 'ii',
+ godot_js_display_clipboard_get: function (callback) {
+ const func = GodotRuntime.get_func(callback);
+ try {
+ navigator.clipboard.readText().then(function (result) {
+ const ptr = GodotRuntime.allocString(result);
+ func(ptr);
+ GodotRuntime.free(ptr);
+ }).catch(function (e) {
+ // Fail graciously.
+ });
+ } catch (e) {
+ // Fail graciously.
+ }
+ },
+
+ /*
+ * Window
+ */
+ godot_js_display_window_title_set__sig: 'vi',
+ godot_js_display_window_title_set: function (p_data) {
+ document.title = GodotRuntime.parseString(p_data);
+ },
+
+ godot_js_display_window_icon_set__sig: 'vii',
+ godot_js_display_window_icon_set: function (p_ptr, p_len) {
+ let link = document.getElementById('-gd-engine-icon');
+ if (link === null) {
+ link = document.createElement('link');
+ link.rel = 'icon';
+ link.id = '-gd-engine-icon';
+ document.head.appendChild(link);
+ }
+ const old_icon = GodotDisplay.window_icon;
+ const png = new Blob([GodotRuntime.heapSlice(HEAPU8, p_ptr, p_len)], { type: 'image/png' });
+ GodotDisplay.window_icon = URL.createObjectURL(png);
+ link.href = GodotDisplay.window_icon;
+ if (old_icon) {
+ URL.revokeObjectURL(old_icon);
+ }
+ },
+
+ /*
+ * Cursor
+ */
+ godot_js_display_cursor_set_visible__sig: 'vi',
+ godot_js_display_cursor_set_visible: function (p_visible) {
+ const visible = p_visible !== 0;
+ if (visible === GodotDisplayCursor.visible) {
+ return;
+ }
+ GodotDisplayCursor.visible = visible;
+ if (visible) {
+ GodotDisplayCursor.set_shape(GodotDisplayCursor.shape);
+ } else {
+ GodotDisplayCursor.set_style('none');
+ }
+ },
+
+ godot_js_display_cursor_is_hidden__sig: 'i',
+ godot_js_display_cursor_is_hidden: function () {
+ return !GodotDisplayCursor.visible;
+ },
+
+ godot_js_display_cursor_set_shape__sig: 'vi',
+ godot_js_display_cursor_set_shape: function (p_string) {
+ GodotDisplayCursor.set_shape(GodotRuntime.parseString(p_string));
+ },
+
+ godot_js_display_cursor_set_custom_shape__sig: 'viiiii',
+ godot_js_display_cursor_set_custom_shape: function (p_shape, p_ptr, p_len, p_hotspot_x, p_hotspot_y) {
+ const shape = GodotRuntime.parseString(p_shape);
+ const old_shape = GodotDisplayCursor.cursors[shape];
+ if (p_len > 0) {
+ const png = new Blob([GodotRuntime.heapSlice(HEAPU8, p_ptr, p_len)], { type: 'image/png' });
+ const url = URL.createObjectURL(png);
+ GodotDisplayCursor.cursors[shape] = {
+ url: url,
+ x: p_hotspot_x,
+ y: p_hotspot_y,
+ };
+ } else {
+ delete GodotDisplayCursor.cursors[shape];
+ }
+ if (shape === GodotDisplayCursor.shape) {
+ GodotDisplayCursor.set_shape(GodotDisplayCursor.shape);
+ }
+ if (old_shape) {
+ URL.revokeObjectURL(old_shape.url);
+ }
+ },
+
+ godot_js_display_cursor_lock_set__sig: 'vi',
+ godot_js_display_cursor_lock_set: function (p_lock) {
+ if (p_lock) {
+ GodotDisplayCursor.lockPointer();
+ } else {
+ GodotDisplayCursor.releasePointer();
+ }
+ },
+
+ godot_js_display_cursor_is_locked__sig: 'i',
+ godot_js_display_cursor_is_locked: function () {
+ return GodotDisplayCursor.isPointerLocked() ? 1 : 0;
+ },
+
+ /*
+ * Listeners
+ */
+ godot_js_display_fullscreen_cb__sig: 'vi',
+ godot_js_display_fullscreen_cb: function (callback) {
+ const canvas = GodotConfig.canvas;
+ const func = GodotRuntime.get_func(callback);
+ function change_cb(evt) {
+ if (evt.target === canvas) {
+ func(GodotDisplayScreen.isFullscreen());
+ }
+ }
+ GodotEventListeners.add(document, 'fullscreenchange', change_cb, false);
+ GodotEventListeners.add(document, 'mozfullscreenchange', change_cb, false);
+ GodotEventListeners.add(document, 'webkitfullscreenchange', change_cb, false);
+ },
+
+ godot_js_display_window_blur_cb__sig: 'vi',
+ godot_js_display_window_blur_cb: function (callback) {
+ const func = GodotRuntime.get_func(callback);
+ GodotEventListeners.add(window, 'blur', function () {
+ func();
+ }, false);
+ },
+
+ godot_js_display_notification_cb__sig: 'viiiii',
+ godot_js_display_notification_cb: function (callback, p_enter, p_exit, p_in, p_out) {
+ const canvas = GodotConfig.canvas;
+ const func = GodotRuntime.get_func(callback);
+ const notif = [p_enter, p_exit, p_in, p_out];
+ ['mouseover', 'mouseleave', 'focus', 'blur'].forEach(function (evt_name, idx) {
+ GodotEventListeners.add(canvas, evt_name, function () {
+ func(notif[idx]);
+ }, true);
+ });
+ },
+
+ godot_js_display_setup_canvas__sig: 'viiii',
+ godot_js_display_setup_canvas: function (p_width, p_height, p_fullscreen, p_hidpi) {
+ const canvas = GodotConfig.canvas;
+ GodotEventListeners.add(canvas, 'contextmenu', function (ev) {
+ ev.preventDefault();
+ }, false);
+ GodotEventListeners.add(canvas, 'webglcontextlost', function (ev) {
+ alert('WebGL context lost, please reload the page'); // eslint-disable-line no-alert
+ ev.preventDefault();
+ }, false);
+ GodotDisplayScreen.hidpi = !!p_hidpi;
+ switch (GodotConfig.canvas_resize_policy) {
+ case 0: // None
+ GodotDisplayScreen.desired_size = [canvas.width, canvas.height];
+ break;
+ case 1: // Project
+ GodotDisplayScreen.desired_size = [p_width, p_height];
+ break;
+ default: // Full window
+ // Ensure we display in the right place, the size will be handled by updateSize
+ canvas.style.position = 'absolute';
+ canvas.style.top = 0;
+ canvas.style.left = 0;
+ break;
+ }
+ GodotDisplayScreen.updateSize();
+ if (p_fullscreen) {
+ GodotDisplayScreen.requestFullscreen();
+ }
+ },
+
+ /*
+ * Virtual Keyboard
+ */
+ godot_js_display_vk_show__sig: 'viiii',
+ godot_js_display_vk_show: function (p_text, p_multiline, p_start, p_end) {
+ const text = GodotRuntime.parseString(p_text);
+ const start = p_start > 0 ? p_start : 0;
+ const end = p_end > 0 ? p_end : start;
+ GodotDisplayVK.show(text, p_multiline, start, end);
+ },
+
+ godot_js_display_vk_hide__sig: 'v',
+ godot_js_display_vk_hide: function () {
+ GodotDisplayVK.hide();
+ },
+
+ godot_js_display_vk_available__sig: 'i',
+ godot_js_display_vk_available: function () {
+ return GodotDisplayVK.available();
+ },
+
+ godot_js_display_tts_available__sig: 'i',
+ godot_js_display_tts_available: function () {
+ return 'speechSynthesis' in window;
+ },
+
+ godot_js_display_vk_cb__sig: 'vi',
+ godot_js_display_vk_cb: function (p_input_cb) {
+ const input_cb = GodotRuntime.get_func(p_input_cb);
+ if (GodotDisplayVK.available()) {
+ GodotDisplayVK.init(input_cb);
+ }
+ },
+};
+
+autoAddDeps(GodotDisplay, '$GodotDisplay');
+mergeInto(LibraryManager.library, GodotDisplay);
diff --git a/platform/javascript/js/libs/library_pandemonium_display.js b/platform/javascript/js/libs/library_pandemonium_display.js
index 0a37fde02..389f3acc2 100644
--- a/platform/javascript/js/libs/library_pandemonium_display.js
+++ b/platform/javascript/js/libs/library_pandemonium_display.js
@@ -81,14 +81,14 @@ const PandemoniumDisplayVK = {
PandemoniumDisplayVK.hide();
}
PandemoniumDisplayVK.updateSize();
- let elem = GodotDisplayVK.textinput;
+ let elem = PandemoniumDisplayVK.textinput;
switch (type) {
case 0: // KEYBOARD_TYPE_DEFAULT
elem.type = 'text';
elem.inputmode = '';
break;
case 1: // KEYBOARD_TYPE_MULTILINE
- elem = GodotDisplayVK.textarea;
+ elem = PandemoniumDisplayVK.textarea;
break;
case 2: // KEYBOARD_TYPE_NUMBER
elem.type = 'text';
@@ -366,6 +366,91 @@ const PandemoniumDisplay = {
return 0;
},
+ pandemonium_js_tts_is_speaking__sig: 'i',
+ pandemonium_js_tts_is_speaking: function () {
+ return window.speechSynthesis.speaking;
+ },
+
+ pandemonium_js_tts_is_paused__sig: 'i',
+ pandemonium_js_tts_is_paused: function () {
+ return window.speechSynthesis.paused;
+ },
+
+ pandemonium_js_tts_get_voices__sig: 'vi',
+ pandemonium_js_tts_get_voices: function (p_callback) {
+ const func = PandemoniumRuntime.get_func(p_callback);
+ try {
+ const arr = [];
+ const voices = window.speechSynthesis.getVoices();
+ for (let i = 0; i < voices.length; i++) {
+ arr.push(`${voices[i].lang};${voices[i].name}`);
+ }
+ const c_ptr = PandemoniumRuntime.allocStringArray(arr);
+ func(arr.length, c_ptr);
+ PandemoniumRuntime.freeStringArray(c_ptr, arr.length);
+ } catch (e) {
+ // Fail graciously.
+ }
+ },
+
+ pandemonium_js_tts_speak__sig: 'viiiffii',
+ pandemonium_js_tts_speak: function (p_text, p_voice, p_volume, p_pitch, p_rate, p_utterance_id, p_callback) {
+ const func = PandemoniumRuntime.get_func(p_callback);
+
+ function listener_end(evt) {
+ evt.currentTarget.cb(1 /*TTS_UTTERANCE_ENDED*/, evt.currentTarget.id, 0);
+ }
+
+ function listener_start(evt) {
+ evt.currentTarget.cb(0 /*TTS_UTTERANCE_STARTED*/, evt.currentTarget.id, 0);
+ }
+
+ function listener_error(evt) {
+ evt.currentTarget.cb(2 /*TTS_UTTERANCE_CANCELED*/, evt.currentTarget.id, 0);
+ }
+
+ function listener_bound(evt) {
+ evt.currentTarget.cb(3 /*TTS_UTTERANCE_BOUNDARY*/, evt.currentTarget.id, evt.charIndex);
+ }
+
+ const utterance = new SpeechSynthesisUtterance(PandemoniumRuntime.parseString(p_text));
+ utterance.rate = p_rate;
+ utterance.pitch = p_pitch;
+ utterance.volume = p_volume / 100.0;
+ utterance.addEventListener('end', listener_end);
+ utterance.addEventListener('start', listener_start);
+ utterance.addEventListener('error', listener_error);
+ utterance.addEventListener('boundary', listener_bound);
+ utterance.id = p_utterance_id;
+ utterance.cb = func;
+ const voice = PandemoniumRuntime.parseString(p_voice);
+ const voices = window.speechSynthesis.getVoices();
+ for (let i = 0; i < voices.length; i++) {
+ if (voices[i].name === voice) {
+ utterance.voice = voices[i];
+ break;
+ }
+ }
+ window.speechSynthesis.resume();
+ window.speechSynthesis.speak(utterance);
+ },
+
+ pandemonium_js_tts_pause__sig: 'v',
+ pandemonium_js_tts_pause: function () {
+ window.speechSynthesis.pause();
+ },
+
+ pandemonium_js_tts_resume__sig: 'v',
+ pandemonium_js_tts_resume: function () {
+ window.speechSynthesis.resume();
+ },
+
+ pandemonium_js_tts_stop__sig: 'v',
+ pandemonium_js_tts_stop: function () {
+ window.speechSynthesis.cancel();
+ window.speechSynthesis.resume();
+ },
+
pandemonium_js_display_alert__sig: 'vi',
pandemonium_js_display_alert: function(p_text) {
window.alert(PandemoniumRuntime.parseString(p_text)); // eslint-disable-line no-alert
@@ -679,4 +764,4 @@ const PandemoniumDisplay = {
};
autoAddDeps(PandemoniumDisplay, '$PandemoniumDisplay');
-mergeInto(LibraryManager.library, PandemoniumDisplay);
\ No newline at end of file
+mergeInto(LibraryManager.library, PandemoniumDisplay);
diff --git a/platform/javascript/os_javascript.cpp b/platform/javascript/os_javascript.cpp
index 7d97d86e7..afc57ef76 100644
--- a/platform/javascript/os_javascript.cpp
+++ b/platform/javascript/os_javascript.cpp
@@ -68,6 +68,90 @@ void OS_JavaScript::request_quit_callback() {
}
}
+bool OS_JavaScript::tts_is_speaking() const {
+ return godot_js_tts_is_speaking();
+}
+
+bool OS_JavaScript::tts_is_paused() const {
+ return godot_js_tts_is_paused();
+}
+
+void OS_JavaScript::update_voices_callback(int p_size, const char **p_voice) {
+ get_singleton()->voices.clear();
+ for (int i = 0; i < p_size; i++) {
+ Vector tokens = String::utf8(p_voice[i]).split(";", true, 2);
+ if (tokens.size() == 2) {
+ Dictionary voice_d;
+ voice_d["name"] = tokens[1];
+ voice_d["id"] = tokens[1];
+ voice_d["language"] = tokens[0];
+ get_singleton()->voices.push_back(voice_d);
+ }
+ }
+}
+
+Array OS_JavaScript::tts_get_voices() const {
+ godot_js_tts_get_voices(update_voices_callback);
+ return voices;
+}
+
+void OS_JavaScript::tts_speak(const String &p_text, const String &p_voice, int p_volume, float p_pitch, float p_rate, int p_utterance_id, bool p_interrupt) {
+ if (p_interrupt) {
+ tts_stop();
+ }
+
+ if (p_text.empty()) {
+ tts_post_utterance_event(OS::TTS_UTTERANCE_CANCELED, p_utterance_id);
+ return;
+ }
+
+ CharString string = p_text.utf8();
+ utterance_ids[p_utterance_id] = string;
+
+ godot_js_tts_speak(string.get_data(), p_voice.utf8().get_data(), CLAMP(p_volume, 0, 100), CLAMP(p_pitch, 0.f, 2.f), CLAMP(p_rate, 0.1f, 10.f), p_utterance_id, OS_JavaScript::_js_utterance_callback);
+}
+
+void OS_JavaScript::tts_pause() {
+ godot_js_tts_pause();
+}
+
+void OS_JavaScript::tts_resume() {
+ godot_js_tts_resume();
+}
+
+void OS_JavaScript::tts_stop() {
+ for (Map::Element *E = utterance_ids.front(); E; E = E->next()) {
+ tts_post_utterance_event(OS::TTS_UTTERANCE_CANCELED, E->key());
+ }
+ utterance_ids.clear();
+ godot_js_tts_stop();
+}
+
+void OS_JavaScript::_js_utterance_callback(int p_event, int p_id, int p_pos) {
+ OS_JavaScript *ds = (OS_JavaScript *)OS::get_singleton();
+ if (ds->utterance_ids.has(p_id)) {
+ int pos = 0;
+ if ((TTSUtteranceEvent)p_event == OS::TTS_UTTERANCE_BOUNDARY) {
+ // Convert position from UTF-8 to UTF-32.
+ const CharString &string = ds->utterance_ids[p_id];
+ for (int i = 0; i < MIN(p_pos, string.length()); i++) {
+ uint8_t c = string[i];
+ if ((c & 0xe0) == 0xc0) {
+ i += 1;
+ } else if ((c & 0xf0) == 0xe0) {
+ i += 2;
+ } else if ((c & 0xf8) == 0xf0) {
+ i += 3;
+ }
+ pos++;
+ }
+ } else if ((TTSUtteranceEvent)p_event != OS::TTS_UTTERANCE_STARTED) {
+ ds->utterance_ids.erase(p_id);
+ }
+ ds->tts_post_utterance_event((TTSUtteranceEvent)p_event, p_id, pos);
+ }
+}
+
// Files drop (implemented in JS for now).
void OS_JavaScript::drop_files_callback(char **p_filev, int p_filec) {
OS_JavaScript *os = get_singleton();
diff --git a/platform/javascript/os_javascript.h b/platform/javascript/os_javascript.h
index 4625be87c..b5eacff0e 100644
--- a/platform/javascript/os_javascript.h
+++ b/platform/javascript/os_javascript.h
@@ -99,6 +99,9 @@ private:
bool idb_is_syncing;
bool pwa_is_waiting;
+ Map utterance_ids;
+ Array voices;
+
static void fullscreen_change_callback(int p_fullscreen);
static int mouse_button_callback(int p_pressed, int p_button, double p_x, double p_y, int p_modifiers);
static void mouse_move_callback(double p_x, double p_y, double p_rel_x, double p_rel_y, int p_modifiers);
@@ -121,6 +124,8 @@ private:
static void update_clipboard_callback(const char *p_text);
static void update_pwa_state_callback();
static void ime_callback(int p_type, const char *p_text);
+ static void _js_utterance_callback(int p_event, int p_id, int p_pos);
+ static void update_voices_callback(int p_size, const char **p_voice);
protected:
void resume_audio();
@@ -146,6 +151,15 @@ public:
// Override return type to make writing static callbacks less tedious.
static OS_JavaScript *get_singleton();
+ virtual bool tts_is_speaking() const;
+ virtual bool tts_is_paused() const;
+ virtual Array tts_get_voices() const;
+
+ virtual void tts_speak(const String &p_text, const String &p_voice, int p_volume = 50, float p_pitch = 1.f, float p_rate = 1.f, int p_utterance_id = 0, bool p_interrupt = false);
+ virtual void tts_pause();
+ virtual void tts_resume();
+ virtual void tts_stop();
+
virtual bool has_virtual_keyboard() const;
virtual void show_virtual_keyboard(const String &p_existing_text, const Rect2 &p_screen_rect = Rect2(), VirtualKeyboardType p_type = KEYBOARD_TYPE_DEFAULT, int p_max_input_length = -1, int p_cursor_start = -1, int p_cursor_end = -1);
virtual void hide_virtual_keyboard();
diff --git a/platform/javascript/pandemonium_js.h b/platform/javascript/pandemonium_js.h
index cbcceb17e..6260dc9ca 100644
--- a/platform/javascript/pandemonium_js.h
+++ b/platform/javascript/pandemonium_js.h
@@ -75,6 +75,15 @@ extern int pandemonium_js_input_gamepad_sample_get(int p_idx, float r_btns[16],
extern void pandemonium_js_input_paste_cb(void (*p_callback)(const char *p_text));
extern void pandemonium_js_input_drop_files_cb(void (*p_callback)(char **p_filev, int p_filec));
+// TTS
+extern int pandemonium_js_tts_is_speaking();
+extern int pandemonium_js_tts_is_paused();
+extern int pandemonium_js_tts_get_voices(void (*p_callback)(int p_size, const char **p_voices));
+extern void pandemonium_js_tts_speak(const char *p_text, const char *p_voice, int p_volume, float p_pitch, float p_rate, int p_utterance_id, void (*p_callback)(int p_event, int p_id, int p_pos));
+extern void pandemonium_js_tts_pause();
+extern void pandemonium_js_tts_resume();
+extern void pandemonium_js_tts_stop();
+
// Display
extern int pandemonium_js_display_screen_dpi_get();
extern double pandemonium_js_display_pixel_ratio_get();
@@ -117,6 +126,7 @@ extern void pandemonium_js_display_notification_cb(void (*p_callback)(int p_noti
// Display Virtual Keyboard
extern int pandemonium_js_display_vk_available();
+extern int pandemonium_js_display_tts_available();
extern void pandemonium_js_display_vk_cb(void (*p_input)(const char *p_text, int p_cursor));
extern void pandemonium_js_display_vk_show(const char *p_text, int p_type, int p_start, int p_end);
extern void pandemonium_js_display_vk_hide();
diff --git a/platform/osx/SCsub b/platform/osx/SCsub
index 543ad72d2..d1d436efb 100644
--- a/platform/osx/SCsub
+++ b/platform/osx/SCsub
@@ -10,6 +10,7 @@ files = [
"os_osx.mm",
"pandemonium_main_osx.mm",
"dir_access_osx.mm",
+ "tts_osx.mm",
"joypad_osx.cpp",
"power_osx.cpp",
]
diff --git a/platform/osx/os_osx.h b/platform/osx/os_osx.h
index 203b69353..045bba623 100644
--- a/platform/osx/os_osx.h
+++ b/platform/osx/os_osx.h
@@ -153,6 +153,8 @@ public:
PowerOSX *power_manager;
+ id tts = nullptr;
+
CrashHandler crash_handler;
void _update_window();
@@ -194,6 +196,15 @@ protected:
public:
static OS_OSX *singleton;
+ virtual bool tts_is_speaking() const;
+ virtual bool tts_is_paused() const;
+ virtual Array tts_get_voices() const;
+
+ virtual void tts_speak(const String &p_text, const String &p_voice, int p_volume = 50, float p_pitch = 1.f, float p_rate = 1.f, int p_utterance_id = 0, bool p_interrupt = false);
+ virtual void tts_pause();
+ virtual void tts_resume();
+ virtual void tts_stop();
+
void global_menu_add_item(const String &p_menu, const String &p_label, const Variant &p_signal, const Variant &p_meta);
void global_menu_add_separator(const String &p_menu);
void global_menu_remove_item(const String &p_menu, int p_idx);
diff --git a/platform/osx/os_osx.mm b/platform/osx/os_osx.mm
index 9bd0ef682..77a37cb84 100644
--- a/platform/osx/os_osx.mm
+++ b/platform/osx/os_osx.mm
@@ -64,6 +64,8 @@
#include
#include
+#include "tts_osx.h"
+
#if MAC_OS_X_VERSION_MAX_ALLOWED < 101200
#define NSEventMaskAny NSAnyEventMask
#define NSEventTypeKeyDown NSKeyDown
@@ -1590,6 +1592,41 @@ int OS_OSX::get_current_video_driver() const {
return video_driver_index;
}
+bool OS_OSX::tts_is_speaking() const {
+ ERR_FAIL_COND_V(!tts, false);
+ return [tts isSpeaking];
+}
+
+bool OS_OSX::tts_is_paused() const {
+ ERR_FAIL_COND_V(!tts, false);
+ return [tts isPaused];
+}
+
+Array OS_OSX::tts_get_voices() const {
+ ERR_FAIL_COND_V(!tts, Array());
+ return [tts getVoices];
+}
+
+void OS_OSX::tts_speak(const String &p_text, const String &p_voice, int p_volume, float p_pitch, float p_rate, int p_utterance_id, bool p_interrupt) {
+ ERR_FAIL_COND(!tts);
+ [tts speak:p_text voice:p_voice volume:p_volume pitch:p_pitch rate:p_rate utterance_id:p_utterance_id interrupt:p_interrupt];
+}
+
+void OS_OSX::tts_pause() {
+ ERR_FAIL_COND(!tts);
+ [tts pauseSpeaking];
+}
+
+void OS_OSX::tts_resume() {
+ ERR_FAIL_COND(!tts);
+ [tts resumeSpeaking];
+}
+
+void OS_OSX::tts_stop() {
+ ERR_FAIL_COND(!tts);
+ [tts stopSpeaking];
+}
+
Error OS_OSX::initialize(const VideoMode &p_desired, int p_video_driver, int p_audio_driver) {
/*** OSX INITIALIZATION ***/
/*** OSX INITIALIZATION ***/
@@ -1608,6 +1645,9 @@ Error OS_OSX::initialize(const VideoMode &p_desired, int p_video_driver, int p_a
// Register to be notified on displays arrangement changes
CGDisplayRegisterReconfigurationCallback(displays_arrangement_changed, NULL);
+ // Init TTS
+ tts = [[TTS_OSX alloc] init];
+
window_delegate = [[PandemoniumWindowDelegate alloc] init];
// Don't use accumulation buffer support; it's not accelerated
diff --git a/platform/osx/tts_osx.h b/platform/osx/tts_osx.h
new file mode 100644
index 000000000..b4103f964
--- /dev/null
+++ b/platform/osx/tts_osx.h
@@ -0,0 +1,71 @@
+/*************************************************************************/
+/* tts_osx.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TTS_OSX_H
+#define TTS_OSX_H
+
+#include "core/variant/array.h"
+#include "core/containers/list.h"
+#include "core/containers/rb_map.h"
+#include "core/os/os.h"
+#include "core/string/ustring.h"
+
+#import
+
+#if __has_include()
+#import
+#else
+#import
+#endif
+
+@interface TTS_OSX : NSObject {
+ // AVSpeechSynthesizer
+ bool speaking;
+ RBMap ids;
+
+ // NSSpeechSynthesizer
+ bool paused;
+ bool have_utterance;
+ int last_utterance;
+
+ id synth; // NSSpeechSynthesizer or AVSpeechSynthesizer
+ List queue;
+}
+
+- (void)pauseSpeaking;
+- (void)resumeSpeaking;
+- (void)stopSpeaking;
+- (bool)isSpeaking;
+- (bool)isPaused;
+- (void)speak:(const String &)text voice:(const String &)voice volume:(int)volume pitch:(float)pitch rate:(float)rate utterance_id:(int)utterance_id interrupt:(bool)interrupt;
+- (Array)getVoices;
+@end
+
+#endif // TTS_OSX_H
diff --git a/platform/osx/tts_osx.mm b/platform/osx/tts_osx.mm
new file mode 100644
index 000000000..10149cdba
--- /dev/null
+++ b/platform/osx/tts_osx.mm
@@ -0,0 +1,267 @@
+/*************************************************************************/
+/* tts_osx.mm */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#include "tts_osx.h"
+
+@implementation TTS_OSX
+
+- (id)init {
+ self = [super init];
+ self->speaking = false;
+ self->have_utterance = false;
+ self->last_utterance = -1;
+ self->paused = false;
+ if (@available(macOS 10.14, *)) {
+ self->synth = [[AVSpeechSynthesizer alloc] init];
+ [self->synth setDelegate:self];
+ print_verbose("Text-to-Speech: AVSpeechSynthesizer initialized.");
+ } else {
+ self->synth = [[NSSpeechSynthesizer alloc] init];
+ [self->synth setDelegate:self];
+ print_verbose("Text-to-Speech: NSSpeechSynthesizer initialized.");
+ }
+ return self;
+}
+
+// AVSpeechSynthesizer callback (macOS 10.14+)
+
+- (void)speechSynthesizer:(AVSpeechSynthesizer *)av_synth willSpeakRangeOfSpeechString:(NSRange)characterRange utterance:(AVSpeechUtterance *)utterance API_AVAILABLE(macosx(10.14)) {
+ NSString *string = [utterance speechString];
+
+ // Convert from UTF-16 to UTF-32 position.
+ int pos = 0;
+ for (NSUInteger i = 0; i < MIN(characterRange.location, string.length); i++) {
+ unichar c = [string characterAtIndex:i];
+ if ((c & 0xfffffc00) == 0xd800) {
+ i++;
+ }
+ pos++;
+ }
+
+ OS::get_singleton()->tts_post_utterance_event(OS::TTS_UTTERANCE_BOUNDARY, ids[utterance], pos);
+}
+
+// AVSpeechSynthesizer callback (macOS 10.14+)
+
+- (void)speechSynthesizer:(AVSpeechSynthesizer *)av_synth didCancelSpeechUtterance:(AVSpeechUtterance *)utterance API_AVAILABLE(macosx(10.14)) {
+ OS::get_singleton()->tts_post_utterance_event(OS::TTS_UTTERANCE_CANCELED, ids[utterance]);
+ ids.erase(utterance);
+ speaking = false;
+ [self update];
+}
+
+// AVSpeechSynthesizer callback (macOS 10.14+)
+
+- (void)speechSynthesizer:(AVSpeechSynthesizer *)av_synth didFinishSpeechUtterance:(AVSpeechUtterance *)utterance API_AVAILABLE(macosx(10.14)) {
+ OS::get_singleton()->tts_post_utterance_event(OS::TTS_UTTERANCE_ENDED, ids[utterance]);
+ ids.erase(utterance);
+ speaking = false;
+ [self update];
+}
+
+// NSSpeechSynthesizer callback (macOS 10.4+)
+
+- (void)speechSynthesizer:(NSSpeechSynthesizer *)ns_synth willSpeakWord:(NSRange)characterRange ofString:(NSString *)string {
+ if (!paused && have_utterance) {
+ // Convert from UTF-16 to UTF-32 position.
+ int pos = 0;
+ for (NSUInteger i = 0; i < MIN(characterRange.location, string.length); i++) {
+ unichar c = [string characterAtIndex:i];
+ if ((c & 0xfffffc00) == 0xd800) {
+ i++;
+ }
+ pos++;
+ }
+
+ OS::get_singleton()->tts_post_utterance_event(OS::TTS_UTTERANCE_BOUNDARY, last_utterance, pos);
+ }
+}
+
+- (void)speechSynthesizer:(NSSpeechSynthesizer *)ns_synth didFinishSpeaking:(BOOL)success {
+ if (!paused && have_utterance) {
+ if (success) {
+ OS::get_singleton()->tts_post_utterance_event(OS::TTS_UTTERANCE_ENDED, last_utterance);
+ } else {
+ OS::get_singleton()->tts_post_utterance_event(OS::TTS_UTTERANCE_CANCELED, last_utterance);
+ }
+ have_utterance = false;
+ }
+ speaking = false;
+ [self update];
+}
+
+- (void)update {
+ if (!speaking && queue.size() > 0) {
+ OS::TTSUtterance &message = queue.front()->get();
+
+ if (@available(macOS 10.14, *)) {
+ AVSpeechSynthesizer *av_synth = synth;
+ AVSpeechUtterance *new_utterance = [[AVSpeechUtterance alloc] initWithString:[NSString stringWithUTF8String:message.text.utf8().get_data()]];
+ [new_utterance setVoice:[AVSpeechSynthesisVoice voiceWithIdentifier:[NSString stringWithUTF8String:message.voice.utf8().get_data()]]];
+ if (message.rate > 1.f) {
+ [new_utterance setRate:Math::range_lerp(message.rate, 1.f, 10.f, AVSpeechUtteranceDefaultSpeechRate, AVSpeechUtteranceMaximumSpeechRate)];
+ } else if (message.rate < 1.f) {
+ [new_utterance setRate:Math::range_lerp(message.rate, 0.1f, 1.f, AVSpeechUtteranceMinimumSpeechRate, AVSpeechUtteranceDefaultSpeechRate)];
+ }
+ [new_utterance setPitchMultiplier:message.pitch];
+ [new_utterance setVolume:(Math::range_lerp(message.volume, 0.f, 100.f, 0.f, 1.f))];
+
+ ids[new_utterance] = message.id;
+ [av_synth speakUtterance:new_utterance];
+ } else {
+ NSSpeechSynthesizer *ns_synth = synth;
+ [ns_synth setObject:nil forProperty:NSSpeechResetProperty error:nil];
+ [ns_synth setVoice:[NSString stringWithUTF8String:message.voice.utf8().get_data()]];
+ int base_pitch = [[ns_synth objectForProperty:NSSpeechPitchBaseProperty error:nil] intValue];
+ [ns_synth setObject:[NSNumber numberWithInt:(base_pitch * (message.pitch / 2.f + 0.5f))] forProperty:NSSpeechPitchBaseProperty error:nullptr];
+ [ns_synth setVolume:(Math::range_lerp(message.volume, 0.f, 100.f, 0.f, 1.f))];
+ [ns_synth setRate:(message.rate * 200)];
+
+ last_utterance = message.id;
+ have_utterance = true;
+ [ns_synth startSpeakingString:[NSString stringWithUTF8String:message.text.utf8().get_data()]];
+ }
+ queue.pop_front();
+
+ OS::get_singleton()->tts_post_utterance_event(OS::TTS_UTTERANCE_STARTED, message.id);
+ speaking = true;
+ }
+}
+
+- (void)pauseSpeaking {
+ if (@available(macOS 10.14, *)) {
+ AVSpeechSynthesizer *av_synth = synth;
+ [av_synth pauseSpeakingAtBoundary:AVSpeechBoundaryImmediate];
+ } else {
+ NSSpeechSynthesizer *ns_synth = synth;
+ [ns_synth pauseSpeakingAtBoundary:NSSpeechImmediateBoundary];
+ }
+ paused = true;
+}
+
+- (void)resumeSpeaking {
+ if (@available(macOS 10.14, *)) {
+ AVSpeechSynthesizer *av_synth = synth;
+ [av_synth continueSpeaking];
+ } else {
+ NSSpeechSynthesizer *ns_synth = synth;
+ [ns_synth continueSpeaking];
+ }
+ paused = false;
+}
+
+- (void)stopSpeaking {
+ for (List::Element *E = queue.front(); E; E = E->next()) {
+ OS::TTSUtterance &message = E->get();
+ OS::get_singleton()->tts_post_utterance_event(OS::TTS_UTTERANCE_CANCELED, message.id);
+ }
+ queue.clear();
+ if (@available(macOS 10.14, *)) {
+ AVSpeechSynthesizer *av_synth = synth;
+ [av_synth stopSpeakingAtBoundary:AVSpeechBoundaryImmediate];
+ } else {
+ NSSpeechSynthesizer *ns_synth = synth;
+ if (have_utterance) {
+ OS::get_singleton()->tts_post_utterance_event(OS::TTS_UTTERANCE_CANCELED, last_utterance);
+ }
+ [ns_synth stopSpeaking];
+ }
+ have_utterance = false;
+ speaking = false;
+ paused = false;
+}
+
+- (bool)isSpeaking {
+ return speaking || (queue.size() > 0);
+}
+
+- (bool)isPaused {
+ if (@available(macOS 10.14, *)) {
+ AVSpeechSynthesizer *av_synth = synth;
+ return [av_synth isPaused];
+ } else {
+ return paused;
+ }
+}
+
+- (void)speak:(const String &)text voice:(const String &)voice volume:(int)volume pitch:(float)pitch rate:(float)rate utterance_id:(int)utterance_id interrupt:(bool)interrupt {
+ if (interrupt) {
+ [self stopSpeaking];
+ }
+
+ if (text.empty()) {
+ OS::get_singleton()->tts_post_utterance_event(OS::TTS_UTTERANCE_CANCELED, utterance_id);
+ return;
+ }
+
+ OS::TTSUtterance message;
+ message.text = text;
+ message.voice = voice;
+ message.volume = CLAMP(volume, 0, 100);
+ message.pitch = CLAMP(pitch, 0.f, 2.f);
+ message.rate = CLAMP(rate, 0.1f, 10.f);
+ message.id = utterance_id;
+ queue.push_back(message);
+
+ if ([self isPaused]) {
+ [self resumeSpeaking];
+ } else {
+ [self update];
+ }
+}
+
+- (Array)getVoices {
+ Array list;
+ if (@available(macOS 10.14, *)) {
+ for (AVSpeechSynthesisVoice *voice in [AVSpeechSynthesisVoice speechVoices]) {
+ NSString *voiceIdentifierString = [voice identifier];
+ NSString *voiceLocaleIdentifier = [voice language];
+ NSString *voiceName = [voice name];
+ Dictionary voice_d;
+ voice_d["name"] = String::utf8([voiceName UTF8String]);
+ voice_d["id"] = String::utf8([voiceIdentifierString UTF8String]);
+ voice_d["language"] = String::utf8([voiceLocaleIdentifier UTF8String]);
+ list.push_back(voice_d);
+ }
+ } else {
+ for (NSString *voiceIdentifierString in [NSSpeechSynthesizer availableVoices]) {
+ NSString *voiceLocaleIdentifier = [[NSSpeechSynthesizer attributesForVoice:voiceIdentifierString] objectForKey:NSVoiceLocaleIdentifier];
+ NSString *voiceName = [[NSSpeechSynthesizer attributesForVoice:voiceIdentifierString] objectForKey:NSVoiceName];
+ Dictionary voice_d;
+ voice_d["name"] = String([voiceName UTF8String]);
+ voice_d["id"] = String([voiceIdentifierString UTF8String]);
+ voice_d["language"] = String([voiceLocaleIdentifier UTF8String]);
+ list.push_back(voice_d);
+ }
+ }
+ return list;
+}
+
+@end
diff --git a/platform/windows/SCsub b/platform/windows/SCsub
index 325e70513..db7cb9d41 100644
--- a/platform/windows/SCsub
+++ b/platform/windows/SCsub
@@ -13,6 +13,7 @@ common_win = [
"os_windows.cpp",
"key_mapping_windows.cpp",
"joypad_windows.cpp",
+ "tts_windows.cpp",
"power_windows.cpp",
"windows_terminal_logger.cpp",
"sub_process_windows.cpp",
diff --git a/platform/windows/detect.py b/platform/windows/detect.py
index f27af6618..69985f708 100644
--- a/platform/windows/detect.py
+++ b/platform/windows/detect.py
@@ -259,6 +259,7 @@ def configure_msvc(env, manual_msvc_config):
"kernel32",
"ole32",
"oleaut32",
+ "sapi",
"user32",
"gdi32",
"IPHLPAPI",
@@ -443,6 +444,7 @@ def configure_mingw(env):
"ws2_32",
"kernel32",
"oleaut32",
+ "sapi",
"dinput8",
"dxguid",
"ksuser",
diff --git a/platform/windows/os_windows.cpp b/platform/windows/os_windows.cpp
index e29c11f14..cab236298 100644
--- a/platform/windows/os_windows.cpp
+++ b/platform/windows/os_windows.cpp
@@ -262,6 +262,41 @@ void OS_Windows::_touch_event(bool p_pressed, float p_x, float p_y, int idx) {
}
};
+bool OS_Windows::tts_is_speaking() const {
+ ERR_FAIL_COND_V(!tts, false);
+ return tts->is_speaking();
+}
+
+bool OS_Windows::tts_is_paused() const {
+ ERR_FAIL_COND_V(!tts, false);
+ return tts->is_paused();
+}
+
+Array OS_Windows::tts_get_voices() const {
+ ERR_FAIL_COND_V(!tts, Array());
+ return tts->get_voices();
+}
+
+void OS_Windows::tts_speak(const String &p_text, const String &p_voice, int p_volume, float p_pitch, float p_rate, int p_utterance_id, bool p_interrupt) {
+ ERR_FAIL_COND(!tts);
+ tts->speak(p_text, p_voice, p_volume, p_pitch, p_rate, p_utterance_id, p_interrupt);
+}
+
+void OS_Windows::tts_pause() {
+ ERR_FAIL_COND(!tts);
+ tts->pause();
+}
+
+void OS_Windows::tts_resume() {
+ ERR_FAIL_COND(!tts);
+ tts->resume();
+}
+
+void OS_Windows::tts_stop() {
+ ERR_FAIL_COND(!tts);
+ tts->stop();
+}
+
void OS_Windows::_drag_event(float p_x, float p_y, int idx) {
RBMap::Element *curr = touch_state.find(idx);
// Defensive
@@ -1360,6 +1395,9 @@ Error OS_Windows::initialize(const VideoMode &p_desired, int p_video_driver, int
return ERR_UNAVAILABLE;
}
+ // Init TTS
+ tts = memnew(TTS_Windows);
+
use_raw_input = true;
RAWINPUTDEVICE Rid[1];
@@ -1813,6 +1851,11 @@ void OS_Windows::finalize() {
if (restore_mouse_trails > 1) {
SystemParametersInfoA(SPI_SETMOUSETRAILS, restore_mouse_trails, 0, 0);
}
+
+ if (tts) {
+ memdelete(tts);
+ }
+ CoUninitialize();
}
void OS_Windows::finalize_core() {
diff --git a/platform/windows/os_windows.h b/platform/windows/os_windows.h
index 561d2fb58..e7634eb01 100644
--- a/platform/windows/os_windows.h
+++ b/platform/windows/os_windows.h
@@ -49,6 +49,7 @@
#ifdef XAUDIO2_ENABLED
#include "drivers/xaudio2/audio_driver_xaudio2.h"
#endif
+#include "tts_windows.h"
#include
#include
@@ -314,6 +315,8 @@ class OS_Windows : public OS {
uint64_t ticks_start;
uint64_t ticks_per_second;
+ TTS_Windows *tts = nullptr;
+
bool old_invalid;
bool outside;
int old_x, old_y;
@@ -440,6 +443,15 @@ public:
void set_mouse_mode(MouseMode p_mode);
MouseMode get_mouse_mode() const;
+ virtual bool tts_is_speaking() const;
+ virtual bool tts_is_paused() const;
+ virtual Array tts_get_voices() const;
+
+ virtual void tts_speak(const String &p_text, const String &p_voice, int p_volume = 50, float p_pitch = 1.f, float p_rate = 1.f, int p_utterance_id = 0, bool p_interrupt = false);
+ virtual void tts_pause();
+ virtual void tts_resume();
+ virtual void tts_stop();
+
virtual void warp_mouse_position(const Point2 &p_to);
virtual Point2 get_mouse_position() const;
void update_real_mouse_position();
diff --git a/platform/windows/tts_windows.cpp b/platform/windows/tts_windows.cpp
new file mode 100644
index 000000000..d9a0d29b9
--- /dev/null
+++ b/platform/windows/tts_windows.cpp
@@ -0,0 +1,261 @@
+/*************************************************************************/
+/* tts_windows.cpp */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#include "tts_windows.h"
+
+TTS_Windows *TTS_Windows::singleton = nullptr;
+
+void __stdcall TTS_Windows::speech_event_callback(WPARAM wParam, LPARAM lParam) {
+ TTS_Windows *tts = TTS_Windows::get_singleton();
+ SPEVENT event;
+ while (tts->synth->GetEvents(1, &event, NULL) == S_OK) {
+ if (tts->ids.has(event.ulStreamNum)) {
+ if (event.eEventId == SPEI_START_INPUT_STREAM) {
+ OS::get_singleton()->tts_post_utterance_event(OS::TTS_UTTERANCE_STARTED, tts->ids[event.ulStreamNum].id);
+ } else if (event.eEventId == SPEI_END_INPUT_STREAM) {
+ OS::get_singleton()->tts_post_utterance_event(OS::TTS_UTTERANCE_ENDED, tts->ids[event.ulStreamNum].id);
+ tts->ids.erase(event.ulStreamNum);
+ tts->_update_tts();
+ } else if (event.eEventId == SPEI_WORD_BOUNDARY) {
+ OS::get_singleton()->tts_post_utterance_event(OS::TTS_UTTERANCE_BOUNDARY, tts->ids[event.ulStreamNum].id, event.lParam - tts->ids[event.ulStreamNum].offset);
+ }
+ }
+ }
+}
+
+void TTS_Windows::_update_tts() {
+ if (!is_speaking() && !paused && queue.size() > 0) {
+ OS::TTSUtterance &message = queue.front()->get();
+
+ String text;
+ DWORD flags = SPF_ASYNC | SPF_PURGEBEFORESPEAK | SPF_IS_XML;
+ String pitch_tag = String("");
+ text = pitch_tag + message.text + String("");
+
+ IEnumSpObjectTokens *cpEnum;
+ ISpObjectToken *cpVoiceToken;
+ ULONG ulCount = 0;
+ ULONG stream_number = 0;
+ ISpObjectTokenCategory *cpCategory;
+ HRESULT hr = CoCreateInstance(CLSID_SpObjectTokenCategory, nullptr, CLSCTX_INPROC_SERVER, IID_ISpObjectTokenCategory, (void **)&cpCategory);
+ if (SUCCEEDED(hr)) {
+ hr = cpCategory->SetId(SPCAT_VOICES, false);
+ if (SUCCEEDED(hr)) {
+ hr = cpCategory->EnumTokens(nullptr, nullptr, &cpEnum);
+ if (SUCCEEDED(hr)) {
+ hr = cpEnum->GetCount(&ulCount);
+ while (SUCCEEDED(hr) && ulCount--) {
+ wchar_t *w_id = 0L;
+ hr = cpEnum->Next(1, &cpVoiceToken, nullptr);
+ cpVoiceToken->GetId(&w_id);
+ if (String((const wchar_t *)w_id) == message.voice) {
+ synth->SetVoice(cpVoiceToken);
+ cpVoiceToken->Release();
+ break;
+ }
+ cpVoiceToken->Release();
+ }
+ cpEnum->Release();
+ }
+ }
+ cpCategory->Release();
+ }
+
+ UTData ut;
+ ut.string = text;
+ ut.offset = pitch_tag.length(); // Substract injected tag offset.
+ ut.id = message.id;
+
+ synth->SetVolume(message.volume);
+ synth->SetRate(10.f * log10(message.rate) / log10(3.f));
+ synth->Speak((LPCWSTR)ut.string.ptr(), flags, &stream_number);
+
+ ids[stream_number] = ut;
+
+ queue.pop_front();
+ }
+}
+
+bool TTS_Windows::is_speaking() const {
+ ERR_FAIL_COND_V(!synth, false);
+
+ SPVOICESTATUS status;
+ synth->GetStatus(&status, nullptr);
+ return (status.dwRunningState == SPRS_IS_SPEAKING);
+}
+
+bool TTS_Windows::is_paused() const {
+ ERR_FAIL_COND_V(!synth, false);
+ return paused;
+}
+
+Array TTS_Windows::get_voices() const {
+ Array list;
+ IEnumSpObjectTokens *cpEnum;
+ ISpObjectToken *cpVoiceToken;
+ ISpDataKey *cpDataKeyAttribs;
+ ULONG ulCount = 0;
+ ISpObjectTokenCategory *cpCategory;
+ HRESULT hr = CoCreateInstance(CLSID_SpObjectTokenCategory, nullptr, CLSCTX_INPROC_SERVER, IID_ISpObjectTokenCategory, (void **)&cpCategory);
+ if (SUCCEEDED(hr)) {
+ hr = cpCategory->SetId(SPCAT_VOICES, false);
+ if (SUCCEEDED(hr)) {
+ hr = cpCategory->EnumTokens(nullptr, nullptr, &cpEnum);
+ if (SUCCEEDED(hr)) {
+ hr = cpEnum->GetCount(&ulCount);
+ while (SUCCEEDED(hr) && ulCount--) {
+ hr = cpEnum->Next(1, &cpVoiceToken, nullptr);
+ HRESULT hr_attr = cpVoiceToken->OpenKey(SPTOKENKEY_ATTRIBUTES, &cpDataKeyAttribs);
+ if (SUCCEEDED(hr_attr)) {
+ wchar_t *w_id = nullptr;
+ wchar_t *w_lang = nullptr;
+ wchar_t *w_name = nullptr;
+ cpVoiceToken->GetId(&w_id);
+ cpDataKeyAttribs->GetStringValue(L"Language", &w_lang);
+ cpDataKeyAttribs->GetStringValue(nullptr, &w_name);
+ LCID locale = wcstol(w_lang, nullptr, 16);
+
+ int locale_chars = GetLocaleInfoW(locale, LOCALE_SISO639LANGNAME, nullptr, 0);
+ int region_chars = GetLocaleInfoW(locale, LOCALE_SISO3166CTRYNAME, nullptr, 0);
+ wchar_t *w_lang_code = new wchar_t[locale_chars];
+ wchar_t *w_reg_code = new wchar_t[region_chars];
+ GetLocaleInfoW(locale, LOCALE_SISO639LANGNAME, w_lang_code, locale_chars);
+ GetLocaleInfoW(locale, LOCALE_SISO3166CTRYNAME, w_reg_code, region_chars);
+
+ Dictionary voice_d;
+ voice_d["id"] = String((const wchar_t *)w_id);
+ if (w_name) {
+ voice_d["name"] = String((const wchar_t *)w_name);
+ } else {
+ voice_d["name"] = voice_d["id"].operator String().replace("HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Speech\\Voices\\Tokens\\", "");
+ }
+ voice_d["language"] = String((const wchar_t *)w_lang_code) + "_" + String((const wchar_t *)w_reg_code);
+ list.push_back(voice_d);
+
+ delete[] w_lang_code;
+ delete[] w_reg_code;
+
+ cpDataKeyAttribs->Release();
+ }
+ cpVoiceToken->Release();
+ }
+ cpEnum->Release();
+ }
+ }
+ cpCategory->Release();
+ }
+ return list;
+}
+
+void TTS_Windows::speak(const String &p_text, const String &p_voice, int p_volume, float p_pitch, float p_rate, int p_utterance_id, bool p_interrupt) {
+ ERR_FAIL_COND(!synth);
+ if (p_interrupt) {
+ stop();
+ }
+
+ if (p_text.empty()) {
+ OS::get_singleton()->tts_post_utterance_event(OS::TTS_UTTERANCE_CANCELED, p_utterance_id);
+ return;
+ }
+
+ OS::TTSUtterance message;
+ message.text = p_text;
+ message.voice = p_voice;
+ message.volume = CLAMP(p_volume, 0, 100);
+ message.pitch = CLAMP(p_pitch, 0.f, 2.f);
+ message.rate = CLAMP(p_rate, 0.1f, 10.f);
+ message.id = p_utterance_id;
+ queue.push_back(message);
+
+ if (is_paused()) {
+ resume();
+ } else {
+ _update_tts();
+ }
+}
+
+void TTS_Windows::pause() {
+ ERR_FAIL_COND(!synth);
+ if (!paused) {
+ if (synth->Pause() == S_OK) {
+ paused = true;
+ }
+ }
+}
+
+void TTS_Windows::resume() {
+ ERR_FAIL_COND(!synth);
+ synth->Resume();
+ paused = false;
+}
+
+void TTS_Windows::stop() {
+ ERR_FAIL_COND(!synth);
+
+ SPVOICESTATUS status;
+ synth->GetStatus(&status, nullptr);
+ if (ids.has(status.ulCurrentStream)) {
+ OS::get_singleton()->tts_post_utterance_event(OS::TTS_UTTERANCE_CANCELED, ids[status.ulCurrentStream].id);
+ ids.erase(status.ulCurrentStream);
+ }
+ for (List::Element *E = queue.front(); E; E = E->next()) {
+ OS::TTSUtterance &message = E->get();
+ OS::get_singleton()->tts_post_utterance_event(OS::TTS_UTTERANCE_CANCELED, message.id);
+ }
+ queue.clear();
+ synth->Speak(nullptr, SPF_PURGEBEFORESPEAK, nullptr);
+ synth->Resume();
+ paused = false;
+}
+
+TTS_Windows *TTS_Windows::get_singleton() {
+ return singleton;
+}
+
+TTS_Windows::TTS_Windows() {
+ singleton = this;
+ CoInitialize(nullptr);
+
+ if (SUCCEEDED(CoCreateInstance(CLSID_SpVoice, nullptr, CLSCTX_ALL, IID_ISpVoice, (void **)&synth))) {
+ ULONGLONG event_mask = SPFEI(SPEI_END_INPUT_STREAM) | SPFEI(SPEI_START_INPUT_STREAM) | SPFEI(SPEI_WORD_BOUNDARY);
+ synth->SetInterest(event_mask, event_mask);
+ synth->SetNotifyCallbackFunction(&speech_event_callback, (WPARAM)(this), 0);
+ print_verbose("Text-to-Speech: SAPI initialized.");
+ } else {
+ print_verbose("Text-to-Speech: Cannot initialize ISpVoice!");
+ }
+}
+
+TTS_Windows::~TTS_Windows() {
+ if (synth) {
+ synth->Release();
+ }
+ singleton = nullptr;
+}
diff --git a/platform/windows/tts_windows.h b/platform/windows/tts_windows.h
new file mode 100644
index 000000000..c0d22fc45
--- /dev/null
+++ b/platform/windows/tts_windows.h
@@ -0,0 +1,80 @@
+/*************************************************************************/
+/* tts_windows.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TTS_WINDOWS_H
+#define TTS_WINDOWS_H
+
+#include "core/array.h"
+#include "core/list.h"
+#include "core/map.h"
+#include "core/os/os.h"
+#include "core/ustring.h"
+
+#include
+#include
+#include
+#include
+
+#define WIN32_LEAN_AND_MEAN
+#include
+
+class TTS_Windows {
+ List queue;
+ ISpVoice *synth = nullptr;
+ bool paused = false;
+ struct UTData {
+ String string;
+ int offset;
+ int id;
+ };
+ Map ids;
+
+ static void __stdcall speech_event_callback(WPARAM wParam, LPARAM lParam);
+ void _update_tts();
+
+ static TTS_Windows *singleton;
+
+public:
+ static TTS_Windows *get_singleton();
+
+ bool is_speaking() const;
+ bool is_paused() const;
+ Array get_voices() const;
+
+ void speak(const String &p_text, const String &p_voice, int p_volume = 50, float p_pitch = 1.f, float p_rate = 1.f, int p_utterance_id = 0, bool p_interrupt = false);
+ void pause();
+ void resume();
+ void stop();
+
+ TTS_Windows();
+ ~TTS_Windows();
+};
+
+#endif // TTS_WINDOWS_H
diff --git a/platform/x11/SCsub b/platform/x11/SCsub
index 5a6e765db..2075f3e4c 100644
--- a/platform/x11/SCsub
+++ b/platform/x11/SCsub
@@ -18,6 +18,9 @@ common_x11 = [
if "udev" in env and env["udev"]:
common_x11.append("libudev-so_wrap.c")
+if "speechd" in env and env["speechd"]:
+ common_x11.append(["speechd-so_wrap.c", "tts_linux.cpp"])
+
prog = env.add_program("#bin/pandemonium", ["pandemonium_x11.cpp"] + common_x11)
if env["debug_symbols"] and env["separate_debug_symbols"]:
diff --git a/platform/x11/detect.py b/platform/x11/detect.py
index f6b6177a0..512e3fc88 100644
--- a/platform/x11/detect.py
+++ b/platform/x11/detect.py
@@ -74,6 +74,7 @@ def get_opts():
BoolVariable("use_tsan", "Use LLVM/GCC compiler thread sanitizer (TSAN))", False),
BoolVariable("use_msan", "Use LLVM/GCC compiler memory sanitizer (MSAN))", False),
BoolVariable("pulseaudio", "Detect and use PulseAudio", True),
+ BoolVariable("speechd", "Detect and use Speech Dispatcher for Text-to-Speech support", True),
BoolVariable("udev", "Use udev for gamepad connection callbacks", True),
BoolVariable("debug_symbols", "Add debugging symbols to release/release_debug builds", True),
BoolVariable("separate_debug_symbols", "Create a separate file containing debugging symbols", False),
@@ -363,6 +364,13 @@ def configure(env):
else:
print("Warning: PulseAudio development libraries not found. Disabling the PulseAudio audio driver.")
+ if env["speechd"]:
+ if os.system("pkg-config --exists speech-dispatcher") == 0: # 0 means found
+ env.Append(CPPDEFINES=["SPEECHD_ENABLED"])
+ env.ParseConfig("pkg-config speech-dispatcher --cflags") # Only cflags, we dlopen the library.
+ else:
+ print("Warning: Speech Dispatcher development libraries not found. Disabling Text-to-Speech support.")
+
if platform.system() == "Linux":
env.Append(CPPDEFINES=["JOYDEV_ENABLED"])
if env["udev"]:
diff --git a/platform/x11/os_x11.cpp b/platform/x11/os_x11.cpp
index 06891098f..850ff985c 100644
--- a/platform/x11/os_x11.cpp
+++ b/platform/x11/os_x11.cpp
@@ -112,6 +112,45 @@ static String get_atom_name(Display *p_disp, Atom p_atom) {
return ret;
}
+#ifdef SPEECHD_ENABLED
+
+bool OS_X11::tts_is_speaking() const {
+ ERR_FAIL_COND_V(!tts, false);
+ return tts->is_speaking();
+}
+
+bool OS_X11::tts_is_paused() const {
+ ERR_FAIL_COND_V(!tts, false);
+ return tts->is_paused();
+}
+
+Array OS_X11::tts_get_voices() const {
+ ERR_FAIL_COND_V(!tts, Array());
+ return tts->get_voices();
+}
+
+void OS_X11::tts_speak(const String &p_text, const String &p_voice, int p_volume, float p_pitch, float p_rate, int p_utterance_id, bool p_interrupt) {
+ ERR_FAIL_COND(!tts);
+ tts->speak(p_text, p_voice, p_volume, p_pitch, p_rate, p_utterance_id, p_interrupt);
+}
+
+void OS_X11::tts_pause() {
+ ERR_FAIL_COND(!tts);
+ tts->pause();
+}
+
+void OS_X11::tts_resume() {
+ ERR_FAIL_COND(!tts);
+ tts->resume();
+}
+
+void OS_X11::tts_stop() {
+ ERR_FAIL_COND(!tts);
+ tts->stop();
+}
+
+#endif
+
void OS_X11::initialize_core() {
crash_handler.initialize();
@@ -381,7 +420,13 @@ Error OS_X11::initialize(const VideoMode &p_desired, int p_video_driver, int p_a
#endif
+#ifdef SPEECHD_ENABLED
+ // Init TTS
+ tts = memnew(TTS_Linux);
+#endif
+
rendering_server = memnew(RenderingServerRaster);
+
if (get_render_thread_mode() != RENDER_THREAD_UNSAFE) {
rendering_server = memnew(RenderingServerWrapMT(rendering_server, get_render_thread_mode() == RENDER_SEPARATE_THREAD));
}
@@ -851,6 +896,10 @@ void OS_X11::finalize() {
driver_alsamidi.close();
#endif
+#ifdef SPEECHD_ENABLED
+ memdelete(tts);
+#endif
+
#ifdef JOYDEV_ENABLED
memdelete(joypad);
#endif
diff --git a/platform/x11/os_x11.h b/platform/x11/os_x11.h
index 580a31a3d..72f9e7b37 100644
--- a/platform/x11/os_x11.h
+++ b/platform/x11/os_x11.h
@@ -53,6 +53,10 @@
#include
#include
+#if defined(SPEECHD_ENABLED)
+#include "tts_linux.h"
+#endif
+
// Hints for X11 fullscreen
typedef struct {
unsigned long flags;
@@ -215,6 +219,10 @@ class OS_X11 : public OS_Unix {
AudioDriverPulseAudio driver_pulseaudio;
#endif
+#ifdef SPEECHD_ENABLED
+ TTS_Linux *tts = nullptr;
+#endif
+
PowerX11 *power_manager;
bool layered_window;
@@ -256,6 +264,17 @@ protected:
public:
virtual String get_name() const;
+#ifdef SPEECHD_ENABLED
+ virtual bool tts_is_speaking() const;
+ virtual bool tts_is_paused() const;
+ virtual Array tts_get_voices() const;
+
+ virtual void tts_speak(const String &p_text, const String &p_voice, int p_volume = 50, float p_pitch = 1.f, float p_rate = 1.f, int p_utterance_id = 0, bool p_interrupt = false);
+ virtual void tts_pause();
+ virtual void tts_resume();
+ virtual void tts_stop();
+#endif
+
virtual void set_cursor_shape(CursorShape p_shape);
virtual CursorShape get_cursor_shape() const;
virtual void set_custom_mouse_cursor(const RES &p_cursor, CursorShape p_shape, const Vector2 &p_hotspot);
diff --git a/platform/x11/speechd-so_wrap.c b/platform/x11/speechd-so_wrap.c
new file mode 100644
index 000000000..749474e18
--- /dev/null
+++ b/platform/x11/speechd-so_wrap.c
@@ -0,0 +1,881 @@
+// This file is generated. Do not edit!
+// see https://github.com/hpvb/dynload-wrapper for details
+// generated by ./dynload-wrapper/generate-wrapper.py 0.3 on 2022-04-28 14:34:21
+// flags: ./dynload-wrapper/generate-wrapper.py --sys-include --include /usr/include/speech-dispatcher/libspeechd.h --soname libspeechd.so.2 --init-name speechd --omit-prefix spd_get_client_list --output-header speechd-so_wrap.h --output-implementation speechd-so_wrap.c
+//
+#include
+
+#define SPDConnectionAddress__free SPDConnectionAddress__free_dylibloader_orig_speechd
+#define spd_get_default_address spd_get_default_address_dylibloader_orig_speechd
+#define spd_open spd_open_dylibloader_orig_speechd
+#define spd_open2 spd_open2_dylibloader_orig_speechd
+#define spd_close spd_close_dylibloader_orig_speechd
+#define spd_say spd_say_dylibloader_orig_speechd
+#define spd_sayf spd_sayf_dylibloader_orig_speechd
+#define spd_stop spd_stop_dylibloader_orig_speechd
+#define spd_stop_all spd_stop_all_dylibloader_orig_speechd
+#define spd_stop_uid spd_stop_uid_dylibloader_orig_speechd
+#define spd_cancel spd_cancel_dylibloader_orig_speechd
+#define spd_cancel_all spd_cancel_all_dylibloader_orig_speechd
+#define spd_cancel_uid spd_cancel_uid_dylibloader_orig_speechd
+#define spd_pause spd_pause_dylibloader_orig_speechd
+#define spd_pause_all spd_pause_all_dylibloader_orig_speechd
+#define spd_pause_uid spd_pause_uid_dylibloader_orig_speechd
+#define spd_resume spd_resume_dylibloader_orig_speechd
+#define spd_resume_all spd_resume_all_dylibloader_orig_speechd
+#define spd_resume_uid spd_resume_uid_dylibloader_orig_speechd
+#define spd_key spd_key_dylibloader_orig_speechd
+#define spd_char spd_char_dylibloader_orig_speechd
+#define spd_wchar spd_wchar_dylibloader_orig_speechd
+#define spd_sound_icon spd_sound_icon_dylibloader_orig_speechd
+#define spd_set_voice_type spd_set_voice_type_dylibloader_orig_speechd
+#define spd_set_voice_type_all spd_set_voice_type_all_dylibloader_orig_speechd
+#define spd_set_voice_type_uid spd_set_voice_type_uid_dylibloader_orig_speechd
+#define spd_get_voice_type spd_get_voice_type_dylibloader_orig_speechd
+#define spd_set_synthesis_voice spd_set_synthesis_voice_dylibloader_orig_speechd
+#define spd_set_synthesis_voice_all spd_set_synthesis_voice_all_dylibloader_orig_speechd
+#define spd_set_synthesis_voice_uid spd_set_synthesis_voice_uid_dylibloader_orig_speechd
+#define spd_set_data_mode spd_set_data_mode_dylibloader_orig_speechd
+#define spd_set_notification_on spd_set_notification_on_dylibloader_orig_speechd
+#define spd_set_notification_off spd_set_notification_off_dylibloader_orig_speechd
+#define spd_set_notification spd_set_notification_dylibloader_orig_speechd
+#define spd_set_voice_rate spd_set_voice_rate_dylibloader_orig_speechd
+#define spd_set_voice_rate_all spd_set_voice_rate_all_dylibloader_orig_speechd
+#define spd_set_voice_rate_uid spd_set_voice_rate_uid_dylibloader_orig_speechd
+#define spd_get_voice_rate spd_get_voice_rate_dylibloader_orig_speechd
+#define spd_set_voice_pitch spd_set_voice_pitch_dylibloader_orig_speechd
+#define spd_set_voice_pitch_all spd_set_voice_pitch_all_dylibloader_orig_speechd
+#define spd_set_voice_pitch_uid spd_set_voice_pitch_uid_dylibloader_orig_speechd
+#define spd_get_voice_pitch spd_get_voice_pitch_dylibloader_orig_speechd
+#define spd_set_voice_pitch_range spd_set_voice_pitch_range_dylibloader_orig_speechd
+#define spd_set_voice_pitch_range_all spd_set_voice_pitch_range_all_dylibloader_orig_speechd
+#define spd_set_voice_pitch_range_uid spd_set_voice_pitch_range_uid_dylibloader_orig_speechd
+#define spd_set_volume spd_set_volume_dylibloader_orig_speechd
+#define spd_set_volume_all spd_set_volume_all_dylibloader_orig_speechd
+#define spd_set_volume_uid spd_set_volume_uid_dylibloader_orig_speechd
+#define spd_get_volume spd_get_volume_dylibloader_orig_speechd
+#define spd_set_punctuation spd_set_punctuation_dylibloader_orig_speechd
+#define spd_set_punctuation_all spd_set_punctuation_all_dylibloader_orig_speechd
+#define spd_set_punctuation_uid spd_set_punctuation_uid_dylibloader_orig_speechd
+#define spd_set_capital_letters spd_set_capital_letters_dylibloader_orig_speechd
+#define spd_set_capital_letters_all spd_set_capital_letters_all_dylibloader_orig_speechd
+#define spd_set_capital_letters_uid spd_set_capital_letters_uid_dylibloader_orig_speechd
+#define spd_set_spelling spd_set_spelling_dylibloader_orig_speechd
+#define spd_set_spelling_all spd_set_spelling_all_dylibloader_orig_speechd
+#define spd_set_spelling_uid spd_set_spelling_uid_dylibloader_orig_speechd
+#define spd_set_language spd_set_language_dylibloader_orig_speechd
+#define spd_set_language_all spd_set_language_all_dylibloader_orig_speechd
+#define spd_set_language_uid spd_set_language_uid_dylibloader_orig_speechd
+#define spd_get_language spd_get_language_dylibloader_orig_speechd
+#define spd_set_output_module spd_set_output_module_dylibloader_orig_speechd
+#define spd_set_output_module_all spd_set_output_module_all_dylibloader_orig_speechd
+#define spd_set_output_module_uid spd_set_output_module_uid_dylibloader_orig_speechd
+#define spd_get_message_list_fd spd_get_message_list_fd_dylibloader_orig_speechd
+#define spd_list_modules spd_list_modules_dylibloader_orig_speechd
+#define free_spd_modules free_spd_modules_dylibloader_orig_speechd
+#define spd_get_output_module spd_get_output_module_dylibloader_orig_speechd
+#define spd_list_voices spd_list_voices_dylibloader_orig_speechd
+#define spd_list_synthesis_voices spd_list_synthesis_voices_dylibloader_orig_speechd
+#define free_spd_voices free_spd_voices_dylibloader_orig_speechd
+#define spd_execute_command_with_list_reply spd_execute_command_with_list_reply_dylibloader_orig_speechd
+#define spd_execute_command spd_execute_command_dylibloader_orig_speechd
+#define spd_execute_command_with_reply spd_execute_command_with_reply_dylibloader_orig_speechd
+#define spd_execute_command_wo_mutex spd_execute_command_wo_mutex_dylibloader_orig_speechd
+#define spd_send_data spd_send_data_dylibloader_orig_speechd
+#define spd_send_data_wo_mutex spd_send_data_wo_mutex_dylibloader_orig_speechd
+#include
+#undef SPDConnectionAddress__free
+#undef spd_get_default_address
+#undef spd_open
+#undef spd_open2
+#undef spd_close
+#undef spd_say
+#undef spd_sayf
+#undef spd_stop
+#undef spd_stop_all
+#undef spd_stop_uid
+#undef spd_cancel
+#undef spd_cancel_all
+#undef spd_cancel_uid
+#undef spd_pause
+#undef spd_pause_all
+#undef spd_pause_uid
+#undef spd_resume
+#undef spd_resume_all
+#undef spd_resume_uid
+#undef spd_key
+#undef spd_char
+#undef spd_wchar
+#undef spd_sound_icon
+#undef spd_set_voice_type
+#undef spd_set_voice_type_all
+#undef spd_set_voice_type_uid
+#undef spd_get_voice_type
+#undef spd_set_synthesis_voice
+#undef spd_set_synthesis_voice_all
+#undef spd_set_synthesis_voice_uid
+#undef spd_set_data_mode
+#undef spd_set_notification_on
+#undef spd_set_notification_off
+#undef spd_set_notification
+#undef spd_set_voice_rate
+#undef spd_set_voice_rate_all
+#undef spd_set_voice_rate_uid
+#undef spd_get_voice_rate
+#undef spd_set_voice_pitch
+#undef spd_set_voice_pitch_all
+#undef spd_set_voice_pitch_uid
+#undef spd_get_voice_pitch
+#undef spd_set_voice_pitch_range
+#undef spd_set_voice_pitch_range_all
+#undef spd_set_voice_pitch_range_uid
+#undef spd_set_volume
+#undef spd_set_volume_all
+#undef spd_set_volume_uid
+#undef spd_get_volume
+#undef spd_set_punctuation
+#undef spd_set_punctuation_all
+#undef spd_set_punctuation_uid
+#undef spd_set_capital_letters
+#undef spd_set_capital_letters_all
+#undef spd_set_capital_letters_uid
+#undef spd_set_spelling
+#undef spd_set_spelling_all
+#undef spd_set_spelling_uid
+#undef spd_set_language
+#undef spd_set_language_all
+#undef spd_set_language_uid
+#undef spd_get_language
+#undef spd_set_output_module
+#undef spd_set_output_module_all
+#undef spd_set_output_module_uid
+#undef spd_get_message_list_fd
+#undef spd_list_modules
+#undef free_spd_modules
+#undef spd_get_output_module
+#undef spd_list_voices
+#undef spd_list_synthesis_voices
+#undef free_spd_voices
+#undef spd_execute_command_with_list_reply
+#undef spd_execute_command
+#undef spd_execute_command_with_reply
+#undef spd_execute_command_wo_mutex
+#undef spd_send_data
+#undef spd_send_data_wo_mutex
+#include
+#include
+void (*SPDConnectionAddress__free_dylibloader_wrapper_speechd)( SPDConnectionAddress*);
+SPDConnectionAddress* (*spd_get_default_address_dylibloader_wrapper_speechd)( char**);
+SPDConnection* (*spd_open_dylibloader_wrapper_speechd)(const char*,const char*,const char*, SPDConnectionMode);
+SPDConnection* (*spd_open2_dylibloader_wrapper_speechd)(const char*,const char*,const char*, SPDConnectionMode, SPDConnectionAddress*, int, char**);
+void (*spd_close_dylibloader_wrapper_speechd)( SPDConnection*);
+int (*spd_say_dylibloader_wrapper_speechd)( SPDConnection*, SPDPriority,const char*);
+int (*spd_sayf_dylibloader_wrapper_speechd)( SPDConnection*, SPDPriority,const char*,...);
+int (*spd_stop_dylibloader_wrapper_speechd)( SPDConnection*);
+int (*spd_stop_all_dylibloader_wrapper_speechd)( SPDConnection*);
+int (*spd_stop_uid_dylibloader_wrapper_speechd)( SPDConnection*, int);
+int (*spd_cancel_dylibloader_wrapper_speechd)( SPDConnection*);
+int (*spd_cancel_all_dylibloader_wrapper_speechd)( SPDConnection*);
+int (*spd_cancel_uid_dylibloader_wrapper_speechd)( SPDConnection*, int);
+int (*spd_pause_dylibloader_wrapper_speechd)( SPDConnection*);
+int (*spd_pause_all_dylibloader_wrapper_speechd)( SPDConnection*);
+int (*spd_pause_uid_dylibloader_wrapper_speechd)( SPDConnection*, int);
+int (*spd_resume_dylibloader_wrapper_speechd)( SPDConnection*);
+int (*spd_resume_all_dylibloader_wrapper_speechd)( SPDConnection*);
+int (*spd_resume_uid_dylibloader_wrapper_speechd)( SPDConnection*, int);
+int (*spd_key_dylibloader_wrapper_speechd)( SPDConnection*, SPDPriority,const char*);
+int (*spd_char_dylibloader_wrapper_speechd)( SPDConnection*, SPDPriority,const char*);
+int (*spd_wchar_dylibloader_wrapper_speechd)( SPDConnection*, SPDPriority, wchar_t);
+int (*spd_sound_icon_dylibloader_wrapper_speechd)( SPDConnection*, SPDPriority,const char*);
+int (*spd_set_voice_type_dylibloader_wrapper_speechd)( SPDConnection*, SPDVoiceType);
+int (*spd_set_voice_type_all_dylibloader_wrapper_speechd)( SPDConnection*, SPDVoiceType);
+int (*spd_set_voice_type_uid_dylibloader_wrapper_speechd)( SPDConnection*, SPDVoiceType, unsigned int);
+SPDVoiceType (*spd_get_voice_type_dylibloader_wrapper_speechd)( SPDConnection*);
+int (*spd_set_synthesis_voice_dylibloader_wrapper_speechd)( SPDConnection*,const char*);
+int (*spd_set_synthesis_voice_all_dylibloader_wrapper_speechd)( SPDConnection*,const char*);
+int (*spd_set_synthesis_voice_uid_dylibloader_wrapper_speechd)( SPDConnection*,const char*, unsigned int);
+int (*spd_set_data_mode_dylibloader_wrapper_speechd)( SPDConnection*, SPDDataMode);
+int (*spd_set_notification_on_dylibloader_wrapper_speechd)( SPDConnection*, SPDNotification);
+int (*spd_set_notification_off_dylibloader_wrapper_speechd)( SPDConnection*, SPDNotification);
+int (*spd_set_notification_dylibloader_wrapper_speechd)( SPDConnection*, SPDNotification,const char*);
+int (*spd_set_voice_rate_dylibloader_wrapper_speechd)( SPDConnection*, signed int);
+int (*spd_set_voice_rate_all_dylibloader_wrapper_speechd)( SPDConnection*, signed int);
+int (*spd_set_voice_rate_uid_dylibloader_wrapper_speechd)( SPDConnection*, signed int, unsigned int);
+int (*spd_get_voice_rate_dylibloader_wrapper_speechd)( SPDConnection*);
+int (*spd_set_voice_pitch_dylibloader_wrapper_speechd)( SPDConnection*, signed int);
+int (*spd_set_voice_pitch_all_dylibloader_wrapper_speechd)( SPDConnection*, signed int);
+int (*spd_set_voice_pitch_uid_dylibloader_wrapper_speechd)( SPDConnection*, signed int, unsigned int);
+int (*spd_get_voice_pitch_dylibloader_wrapper_speechd)( SPDConnection*);
+int (*spd_set_voice_pitch_range_dylibloader_wrapper_speechd)( SPDConnection*, signed int);
+int (*spd_set_voice_pitch_range_all_dylibloader_wrapper_speechd)( SPDConnection*, signed int);
+int (*spd_set_voice_pitch_range_uid_dylibloader_wrapper_speechd)( SPDConnection*, signed int, unsigned int);
+int (*spd_set_volume_dylibloader_wrapper_speechd)( SPDConnection*, signed int);
+int (*spd_set_volume_all_dylibloader_wrapper_speechd)( SPDConnection*, signed int);
+int (*spd_set_volume_uid_dylibloader_wrapper_speechd)( SPDConnection*, signed int, unsigned int);
+int (*spd_get_volume_dylibloader_wrapper_speechd)( SPDConnection*);
+int (*spd_set_punctuation_dylibloader_wrapper_speechd)( SPDConnection*, SPDPunctuation);
+int (*spd_set_punctuation_all_dylibloader_wrapper_speechd)( SPDConnection*, SPDPunctuation);
+int (*spd_set_punctuation_uid_dylibloader_wrapper_speechd)( SPDConnection*, SPDPunctuation, unsigned int);
+int (*spd_set_capital_letters_dylibloader_wrapper_speechd)( SPDConnection*, SPDCapitalLetters);
+int (*spd_set_capital_letters_all_dylibloader_wrapper_speechd)( SPDConnection*, SPDCapitalLetters);
+int (*spd_set_capital_letters_uid_dylibloader_wrapper_speechd)( SPDConnection*, SPDCapitalLetters, unsigned int);
+int (*spd_set_spelling_dylibloader_wrapper_speechd)( SPDConnection*, SPDSpelling);
+int (*spd_set_spelling_all_dylibloader_wrapper_speechd)( SPDConnection*, SPDSpelling);
+int (*spd_set_spelling_uid_dylibloader_wrapper_speechd)( SPDConnection*, SPDSpelling, unsigned int);
+int (*spd_set_language_dylibloader_wrapper_speechd)( SPDConnection*,const char*);
+int (*spd_set_language_all_dylibloader_wrapper_speechd)( SPDConnection*,const char*);
+int (*spd_set_language_uid_dylibloader_wrapper_speechd)( SPDConnection*,const char*, unsigned int);
+char* (*spd_get_language_dylibloader_wrapper_speechd)( SPDConnection*);
+int (*spd_set_output_module_dylibloader_wrapper_speechd)( SPDConnection*,const char*);
+int (*spd_set_output_module_all_dylibloader_wrapper_speechd)( SPDConnection*,const char*);
+int (*spd_set_output_module_uid_dylibloader_wrapper_speechd)( SPDConnection*,const char*, unsigned int);
+int (*spd_get_message_list_fd_dylibloader_wrapper_speechd)( SPDConnection*, int, int*, char**);
+char** (*spd_list_modules_dylibloader_wrapper_speechd)( SPDConnection*);
+void (*free_spd_modules_dylibloader_wrapper_speechd)( char**);
+char* (*spd_get_output_module_dylibloader_wrapper_speechd)( SPDConnection*);
+char** (*spd_list_voices_dylibloader_wrapper_speechd)( SPDConnection*);
+SPDVoice** (*spd_list_synthesis_voices_dylibloader_wrapper_speechd)( SPDConnection*);
+void (*free_spd_voices_dylibloader_wrapper_speechd)( SPDVoice**);
+char** (*spd_execute_command_with_list_reply_dylibloader_wrapper_speechd)( SPDConnection*, char*);
+int (*spd_execute_command_dylibloader_wrapper_speechd)( SPDConnection*, char*);
+int (*spd_execute_command_with_reply_dylibloader_wrapper_speechd)( SPDConnection*, char*, char**);
+int (*spd_execute_command_wo_mutex_dylibloader_wrapper_speechd)( SPDConnection*, char*);
+char* (*spd_send_data_dylibloader_wrapper_speechd)( SPDConnection*,const char*, int);
+char* (*spd_send_data_wo_mutex_dylibloader_wrapper_speechd)( SPDConnection*,const char*, int);
+int initialize_speechd(int verbose) {
+ void *handle;
+ char *error;
+ handle = dlopen("libspeechd.so.2", RTLD_LAZY);
+ if (!handle) {
+ if (verbose) {
+ fprintf(stderr, "%s\n", dlerror());
+ }
+ return(1);
+ }
+ dlerror();
+// SPDConnectionAddress__free
+ *(void **) (&SPDConnectionAddress__free_dylibloader_wrapper_speechd) = dlsym(handle, "SPDConnectionAddress__free");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_get_default_address
+ *(void **) (&spd_get_default_address_dylibloader_wrapper_speechd) = dlsym(handle, "spd_get_default_address");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_open
+ *(void **) (&spd_open_dylibloader_wrapper_speechd) = dlsym(handle, "spd_open");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_open2
+ *(void **) (&spd_open2_dylibloader_wrapper_speechd) = dlsym(handle, "spd_open2");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_close
+ *(void **) (&spd_close_dylibloader_wrapper_speechd) = dlsym(handle, "spd_close");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_say
+ *(void **) (&spd_say_dylibloader_wrapper_speechd) = dlsym(handle, "spd_say");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_sayf
+ *(void **) (&spd_sayf_dylibloader_wrapper_speechd) = dlsym(handle, "spd_sayf");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_stop
+ *(void **) (&spd_stop_dylibloader_wrapper_speechd) = dlsym(handle, "spd_stop");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_stop_all
+ *(void **) (&spd_stop_all_dylibloader_wrapper_speechd) = dlsym(handle, "spd_stop_all");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_stop_uid
+ *(void **) (&spd_stop_uid_dylibloader_wrapper_speechd) = dlsym(handle, "spd_stop_uid");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_cancel
+ *(void **) (&spd_cancel_dylibloader_wrapper_speechd) = dlsym(handle, "spd_cancel");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_cancel_all
+ *(void **) (&spd_cancel_all_dylibloader_wrapper_speechd) = dlsym(handle, "spd_cancel_all");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_cancel_uid
+ *(void **) (&spd_cancel_uid_dylibloader_wrapper_speechd) = dlsym(handle, "spd_cancel_uid");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_pause
+ *(void **) (&spd_pause_dylibloader_wrapper_speechd) = dlsym(handle, "spd_pause");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_pause_all
+ *(void **) (&spd_pause_all_dylibloader_wrapper_speechd) = dlsym(handle, "spd_pause_all");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_pause_uid
+ *(void **) (&spd_pause_uid_dylibloader_wrapper_speechd) = dlsym(handle, "spd_pause_uid");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_resume
+ *(void **) (&spd_resume_dylibloader_wrapper_speechd) = dlsym(handle, "spd_resume");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_resume_all
+ *(void **) (&spd_resume_all_dylibloader_wrapper_speechd) = dlsym(handle, "spd_resume_all");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_resume_uid
+ *(void **) (&spd_resume_uid_dylibloader_wrapper_speechd) = dlsym(handle, "spd_resume_uid");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_key
+ *(void **) (&spd_key_dylibloader_wrapper_speechd) = dlsym(handle, "spd_key");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_char
+ *(void **) (&spd_char_dylibloader_wrapper_speechd) = dlsym(handle, "spd_char");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_wchar
+ *(void **) (&spd_wchar_dylibloader_wrapper_speechd) = dlsym(handle, "spd_wchar");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_sound_icon
+ *(void **) (&spd_sound_icon_dylibloader_wrapper_speechd) = dlsym(handle, "spd_sound_icon");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_set_voice_type
+ *(void **) (&spd_set_voice_type_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_voice_type");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_set_voice_type_all
+ *(void **) (&spd_set_voice_type_all_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_voice_type_all");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_set_voice_type_uid
+ *(void **) (&spd_set_voice_type_uid_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_voice_type_uid");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_get_voice_type
+ *(void **) (&spd_get_voice_type_dylibloader_wrapper_speechd) = dlsym(handle, "spd_get_voice_type");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_set_synthesis_voice
+ *(void **) (&spd_set_synthesis_voice_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_synthesis_voice");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_set_synthesis_voice_all
+ *(void **) (&spd_set_synthesis_voice_all_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_synthesis_voice_all");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_set_synthesis_voice_uid
+ *(void **) (&spd_set_synthesis_voice_uid_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_synthesis_voice_uid");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_set_data_mode
+ *(void **) (&spd_set_data_mode_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_data_mode");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_set_notification_on
+ *(void **) (&spd_set_notification_on_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_notification_on");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_set_notification_off
+ *(void **) (&spd_set_notification_off_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_notification_off");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_set_notification
+ *(void **) (&spd_set_notification_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_notification");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_set_voice_rate
+ *(void **) (&spd_set_voice_rate_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_voice_rate");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_set_voice_rate_all
+ *(void **) (&spd_set_voice_rate_all_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_voice_rate_all");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_set_voice_rate_uid
+ *(void **) (&spd_set_voice_rate_uid_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_voice_rate_uid");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_get_voice_rate
+ *(void **) (&spd_get_voice_rate_dylibloader_wrapper_speechd) = dlsym(handle, "spd_get_voice_rate");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_set_voice_pitch
+ *(void **) (&spd_set_voice_pitch_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_voice_pitch");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_set_voice_pitch_all
+ *(void **) (&spd_set_voice_pitch_all_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_voice_pitch_all");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_set_voice_pitch_uid
+ *(void **) (&spd_set_voice_pitch_uid_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_voice_pitch_uid");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_get_voice_pitch
+ *(void **) (&spd_get_voice_pitch_dylibloader_wrapper_speechd) = dlsym(handle, "spd_get_voice_pitch");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_set_voice_pitch_range
+ *(void **) (&spd_set_voice_pitch_range_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_voice_pitch_range");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_set_voice_pitch_range_all
+ *(void **) (&spd_set_voice_pitch_range_all_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_voice_pitch_range_all");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_set_voice_pitch_range_uid
+ *(void **) (&spd_set_voice_pitch_range_uid_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_voice_pitch_range_uid");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_set_volume
+ *(void **) (&spd_set_volume_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_volume");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_set_volume_all
+ *(void **) (&spd_set_volume_all_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_volume_all");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_set_volume_uid
+ *(void **) (&spd_set_volume_uid_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_volume_uid");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_get_volume
+ *(void **) (&spd_get_volume_dylibloader_wrapper_speechd) = dlsym(handle, "spd_get_volume");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_set_punctuation
+ *(void **) (&spd_set_punctuation_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_punctuation");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_set_punctuation_all
+ *(void **) (&spd_set_punctuation_all_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_punctuation_all");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_set_punctuation_uid
+ *(void **) (&spd_set_punctuation_uid_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_punctuation_uid");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_set_capital_letters
+ *(void **) (&spd_set_capital_letters_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_capital_letters");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_set_capital_letters_all
+ *(void **) (&spd_set_capital_letters_all_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_capital_letters_all");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_set_capital_letters_uid
+ *(void **) (&spd_set_capital_letters_uid_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_capital_letters_uid");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_set_spelling
+ *(void **) (&spd_set_spelling_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_spelling");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_set_spelling_all
+ *(void **) (&spd_set_spelling_all_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_spelling_all");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_set_spelling_uid
+ *(void **) (&spd_set_spelling_uid_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_spelling_uid");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_set_language
+ *(void **) (&spd_set_language_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_language");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_set_language_all
+ *(void **) (&spd_set_language_all_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_language_all");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_set_language_uid
+ *(void **) (&spd_set_language_uid_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_language_uid");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_get_language
+ *(void **) (&spd_get_language_dylibloader_wrapper_speechd) = dlsym(handle, "spd_get_language");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_set_output_module
+ *(void **) (&spd_set_output_module_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_output_module");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_set_output_module_all
+ *(void **) (&spd_set_output_module_all_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_output_module_all");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_set_output_module_uid
+ *(void **) (&spd_set_output_module_uid_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_output_module_uid");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_get_message_list_fd
+ *(void **) (&spd_get_message_list_fd_dylibloader_wrapper_speechd) = dlsym(handle, "spd_get_message_list_fd");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_list_modules
+ *(void **) (&spd_list_modules_dylibloader_wrapper_speechd) = dlsym(handle, "spd_list_modules");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// free_spd_modules
+ *(void **) (&free_spd_modules_dylibloader_wrapper_speechd) = dlsym(handle, "free_spd_modules");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_get_output_module
+ *(void **) (&spd_get_output_module_dylibloader_wrapper_speechd) = dlsym(handle, "spd_get_output_module");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_list_voices
+ *(void **) (&spd_list_voices_dylibloader_wrapper_speechd) = dlsym(handle, "spd_list_voices");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_list_synthesis_voices
+ *(void **) (&spd_list_synthesis_voices_dylibloader_wrapper_speechd) = dlsym(handle, "spd_list_synthesis_voices");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// free_spd_voices
+ *(void **) (&free_spd_voices_dylibloader_wrapper_speechd) = dlsym(handle, "free_spd_voices");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_execute_command_with_list_reply
+ *(void **) (&spd_execute_command_with_list_reply_dylibloader_wrapper_speechd) = dlsym(handle, "spd_execute_command_with_list_reply");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_execute_command
+ *(void **) (&spd_execute_command_dylibloader_wrapper_speechd) = dlsym(handle, "spd_execute_command");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_execute_command_with_reply
+ *(void **) (&spd_execute_command_with_reply_dylibloader_wrapper_speechd) = dlsym(handle, "spd_execute_command_with_reply");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_execute_command_wo_mutex
+ *(void **) (&spd_execute_command_wo_mutex_dylibloader_wrapper_speechd) = dlsym(handle, "spd_execute_command_wo_mutex");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_send_data
+ *(void **) (&spd_send_data_dylibloader_wrapper_speechd) = dlsym(handle, "spd_send_data");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+// spd_send_data_wo_mutex
+ *(void **) (&spd_send_data_wo_mutex_dylibloader_wrapper_speechd) = dlsym(handle, "spd_send_data_wo_mutex");
+ if (verbose) {
+ error = dlerror();
+ if (error != NULL) {
+ fprintf(stderr, "%s\n", error);
+ }
+ }
+return 0;
+}
diff --git a/platform/x11/speechd-so_wrap.h b/platform/x11/speechd-so_wrap.h
new file mode 100644
index 000000000..8e1c05334
--- /dev/null
+++ b/platform/x11/speechd-so_wrap.h
@@ -0,0 +1,330 @@
+#ifndef DYLIBLOAD_WRAPPER_SPEECHD
+#define DYLIBLOAD_WRAPPER_SPEECHD
+// This file is generated. Do not edit!
+// see https://github.com/hpvb/dynload-wrapper for details
+// generated by ./dynload-wrapper/generate-wrapper.py 0.3 on 2022-04-28 14:34:21
+// flags: ./dynload-wrapper/generate-wrapper.py --sys-include --include /usr/include/speech-dispatcher/libspeechd.h --soname libspeechd.so.2 --init-name speechd --omit-prefix spd_get_client_list --output-header speechd-so_wrap.h --output-implementation speechd-so_wrap.c
+//
+#include
+
+#define SPDConnectionAddress__free SPDConnectionAddress__free_dylibloader_orig_speechd
+#define spd_get_default_address spd_get_default_address_dylibloader_orig_speechd
+#define spd_open spd_open_dylibloader_orig_speechd
+#define spd_open2 spd_open2_dylibloader_orig_speechd
+#define spd_close spd_close_dylibloader_orig_speechd
+#define spd_say spd_say_dylibloader_orig_speechd
+#define spd_sayf spd_sayf_dylibloader_orig_speechd
+#define spd_stop spd_stop_dylibloader_orig_speechd
+#define spd_stop_all spd_stop_all_dylibloader_orig_speechd
+#define spd_stop_uid spd_stop_uid_dylibloader_orig_speechd
+#define spd_cancel spd_cancel_dylibloader_orig_speechd
+#define spd_cancel_all spd_cancel_all_dylibloader_orig_speechd
+#define spd_cancel_uid spd_cancel_uid_dylibloader_orig_speechd
+#define spd_pause spd_pause_dylibloader_orig_speechd
+#define spd_pause_all spd_pause_all_dylibloader_orig_speechd
+#define spd_pause_uid spd_pause_uid_dylibloader_orig_speechd
+#define spd_resume spd_resume_dylibloader_orig_speechd
+#define spd_resume_all spd_resume_all_dylibloader_orig_speechd
+#define spd_resume_uid spd_resume_uid_dylibloader_orig_speechd
+#define spd_key spd_key_dylibloader_orig_speechd
+#define spd_char spd_char_dylibloader_orig_speechd
+#define spd_wchar spd_wchar_dylibloader_orig_speechd
+#define spd_sound_icon spd_sound_icon_dylibloader_orig_speechd
+#define spd_set_voice_type spd_set_voice_type_dylibloader_orig_speechd
+#define spd_set_voice_type_all spd_set_voice_type_all_dylibloader_orig_speechd
+#define spd_set_voice_type_uid spd_set_voice_type_uid_dylibloader_orig_speechd
+#define spd_get_voice_type spd_get_voice_type_dylibloader_orig_speechd
+#define spd_set_synthesis_voice spd_set_synthesis_voice_dylibloader_orig_speechd
+#define spd_set_synthesis_voice_all spd_set_synthesis_voice_all_dylibloader_orig_speechd
+#define spd_set_synthesis_voice_uid spd_set_synthesis_voice_uid_dylibloader_orig_speechd
+#define spd_set_data_mode spd_set_data_mode_dylibloader_orig_speechd
+#define spd_set_notification_on spd_set_notification_on_dylibloader_orig_speechd
+#define spd_set_notification_off spd_set_notification_off_dylibloader_orig_speechd
+#define spd_set_notification spd_set_notification_dylibloader_orig_speechd
+#define spd_set_voice_rate spd_set_voice_rate_dylibloader_orig_speechd
+#define spd_set_voice_rate_all spd_set_voice_rate_all_dylibloader_orig_speechd
+#define spd_set_voice_rate_uid spd_set_voice_rate_uid_dylibloader_orig_speechd
+#define spd_get_voice_rate spd_get_voice_rate_dylibloader_orig_speechd
+#define spd_set_voice_pitch spd_set_voice_pitch_dylibloader_orig_speechd
+#define spd_set_voice_pitch_all spd_set_voice_pitch_all_dylibloader_orig_speechd
+#define spd_set_voice_pitch_uid spd_set_voice_pitch_uid_dylibloader_orig_speechd
+#define spd_get_voice_pitch spd_get_voice_pitch_dylibloader_orig_speechd
+#define spd_set_voice_pitch_range spd_set_voice_pitch_range_dylibloader_orig_speechd
+#define spd_set_voice_pitch_range_all spd_set_voice_pitch_range_all_dylibloader_orig_speechd
+#define spd_set_voice_pitch_range_uid spd_set_voice_pitch_range_uid_dylibloader_orig_speechd
+#define spd_set_volume spd_set_volume_dylibloader_orig_speechd
+#define spd_set_volume_all spd_set_volume_all_dylibloader_orig_speechd
+#define spd_set_volume_uid spd_set_volume_uid_dylibloader_orig_speechd
+#define spd_get_volume spd_get_volume_dylibloader_orig_speechd
+#define spd_set_punctuation spd_set_punctuation_dylibloader_orig_speechd
+#define spd_set_punctuation_all spd_set_punctuation_all_dylibloader_orig_speechd
+#define spd_set_punctuation_uid spd_set_punctuation_uid_dylibloader_orig_speechd
+#define spd_set_capital_letters spd_set_capital_letters_dylibloader_orig_speechd
+#define spd_set_capital_letters_all spd_set_capital_letters_all_dylibloader_orig_speechd
+#define spd_set_capital_letters_uid spd_set_capital_letters_uid_dylibloader_orig_speechd
+#define spd_set_spelling spd_set_spelling_dylibloader_orig_speechd
+#define spd_set_spelling_all spd_set_spelling_all_dylibloader_orig_speechd
+#define spd_set_spelling_uid spd_set_spelling_uid_dylibloader_orig_speechd
+#define spd_set_language spd_set_language_dylibloader_orig_speechd
+#define spd_set_language_all spd_set_language_all_dylibloader_orig_speechd
+#define spd_set_language_uid spd_set_language_uid_dylibloader_orig_speechd
+#define spd_get_language spd_get_language_dylibloader_orig_speechd
+#define spd_set_output_module spd_set_output_module_dylibloader_orig_speechd
+#define spd_set_output_module_all spd_set_output_module_all_dylibloader_orig_speechd
+#define spd_set_output_module_uid spd_set_output_module_uid_dylibloader_orig_speechd
+#define spd_get_message_list_fd spd_get_message_list_fd_dylibloader_orig_speechd
+#define spd_list_modules spd_list_modules_dylibloader_orig_speechd
+#define free_spd_modules free_spd_modules_dylibloader_orig_speechd
+#define spd_get_output_module spd_get_output_module_dylibloader_orig_speechd
+#define spd_list_voices spd_list_voices_dylibloader_orig_speechd
+#define spd_list_synthesis_voices spd_list_synthesis_voices_dylibloader_orig_speechd
+#define free_spd_voices free_spd_voices_dylibloader_orig_speechd
+#define spd_execute_command_with_list_reply spd_execute_command_with_list_reply_dylibloader_orig_speechd
+#define spd_execute_command spd_execute_command_dylibloader_orig_speechd
+#define spd_execute_command_with_reply spd_execute_command_with_reply_dylibloader_orig_speechd
+#define spd_execute_command_wo_mutex spd_execute_command_wo_mutex_dylibloader_orig_speechd
+#define spd_send_data spd_send_data_dylibloader_orig_speechd
+#define spd_send_data_wo_mutex spd_send_data_wo_mutex_dylibloader_orig_speechd
+#include
+#undef SPDConnectionAddress__free
+#undef spd_get_default_address
+#undef spd_open
+#undef spd_open2
+#undef spd_close
+#undef spd_say
+#undef spd_sayf
+#undef spd_stop
+#undef spd_stop_all
+#undef spd_stop_uid
+#undef spd_cancel
+#undef spd_cancel_all
+#undef spd_cancel_uid
+#undef spd_pause
+#undef spd_pause_all
+#undef spd_pause_uid
+#undef spd_resume
+#undef spd_resume_all
+#undef spd_resume_uid
+#undef spd_key
+#undef spd_char
+#undef spd_wchar
+#undef spd_sound_icon
+#undef spd_set_voice_type
+#undef spd_set_voice_type_all
+#undef spd_set_voice_type_uid
+#undef spd_get_voice_type
+#undef spd_set_synthesis_voice
+#undef spd_set_synthesis_voice_all
+#undef spd_set_synthesis_voice_uid
+#undef spd_set_data_mode
+#undef spd_set_notification_on
+#undef spd_set_notification_off
+#undef spd_set_notification
+#undef spd_set_voice_rate
+#undef spd_set_voice_rate_all
+#undef spd_set_voice_rate_uid
+#undef spd_get_voice_rate
+#undef spd_set_voice_pitch
+#undef spd_set_voice_pitch_all
+#undef spd_set_voice_pitch_uid
+#undef spd_get_voice_pitch
+#undef spd_set_voice_pitch_range
+#undef spd_set_voice_pitch_range_all
+#undef spd_set_voice_pitch_range_uid
+#undef spd_set_volume
+#undef spd_set_volume_all
+#undef spd_set_volume_uid
+#undef spd_get_volume
+#undef spd_set_punctuation
+#undef spd_set_punctuation_all
+#undef spd_set_punctuation_uid
+#undef spd_set_capital_letters
+#undef spd_set_capital_letters_all
+#undef spd_set_capital_letters_uid
+#undef spd_set_spelling
+#undef spd_set_spelling_all
+#undef spd_set_spelling_uid
+#undef spd_set_language
+#undef spd_set_language_all
+#undef spd_set_language_uid
+#undef spd_get_language
+#undef spd_set_output_module
+#undef spd_set_output_module_all
+#undef spd_set_output_module_uid
+#undef spd_get_message_list_fd
+#undef spd_list_modules
+#undef free_spd_modules
+#undef spd_get_output_module
+#undef spd_list_voices
+#undef spd_list_synthesis_voices
+#undef free_spd_voices
+#undef spd_execute_command_with_list_reply
+#undef spd_execute_command
+#undef spd_execute_command_with_reply
+#undef spd_execute_command_wo_mutex
+#undef spd_send_data
+#undef spd_send_data_wo_mutex
+#ifdef __cplusplus
+extern "C" {
+#endif
+#define SPDConnectionAddress__free SPDConnectionAddress__free_dylibloader_wrapper_speechd
+#define spd_get_default_address spd_get_default_address_dylibloader_wrapper_speechd
+#define spd_open spd_open_dylibloader_wrapper_speechd
+#define spd_open2 spd_open2_dylibloader_wrapper_speechd
+#define spd_close spd_close_dylibloader_wrapper_speechd
+#define spd_say spd_say_dylibloader_wrapper_speechd
+#define spd_sayf spd_sayf_dylibloader_wrapper_speechd
+#define spd_stop spd_stop_dylibloader_wrapper_speechd
+#define spd_stop_all spd_stop_all_dylibloader_wrapper_speechd
+#define spd_stop_uid spd_stop_uid_dylibloader_wrapper_speechd
+#define spd_cancel spd_cancel_dylibloader_wrapper_speechd
+#define spd_cancel_all spd_cancel_all_dylibloader_wrapper_speechd
+#define spd_cancel_uid spd_cancel_uid_dylibloader_wrapper_speechd
+#define spd_pause spd_pause_dylibloader_wrapper_speechd
+#define spd_pause_all spd_pause_all_dylibloader_wrapper_speechd
+#define spd_pause_uid spd_pause_uid_dylibloader_wrapper_speechd
+#define spd_resume spd_resume_dylibloader_wrapper_speechd
+#define spd_resume_all spd_resume_all_dylibloader_wrapper_speechd
+#define spd_resume_uid spd_resume_uid_dylibloader_wrapper_speechd
+#define spd_key spd_key_dylibloader_wrapper_speechd
+#define spd_char spd_char_dylibloader_wrapper_speechd
+#define spd_wchar spd_wchar_dylibloader_wrapper_speechd
+#define spd_sound_icon spd_sound_icon_dylibloader_wrapper_speechd
+#define spd_set_voice_type spd_set_voice_type_dylibloader_wrapper_speechd
+#define spd_set_voice_type_all spd_set_voice_type_all_dylibloader_wrapper_speechd
+#define spd_set_voice_type_uid spd_set_voice_type_uid_dylibloader_wrapper_speechd
+#define spd_get_voice_type spd_get_voice_type_dylibloader_wrapper_speechd
+#define spd_set_synthesis_voice spd_set_synthesis_voice_dylibloader_wrapper_speechd
+#define spd_set_synthesis_voice_all spd_set_synthesis_voice_all_dylibloader_wrapper_speechd
+#define spd_set_synthesis_voice_uid spd_set_synthesis_voice_uid_dylibloader_wrapper_speechd
+#define spd_set_data_mode spd_set_data_mode_dylibloader_wrapper_speechd
+#define spd_set_notification_on spd_set_notification_on_dylibloader_wrapper_speechd
+#define spd_set_notification_off spd_set_notification_off_dylibloader_wrapper_speechd
+#define spd_set_notification spd_set_notification_dylibloader_wrapper_speechd
+#define spd_set_voice_rate spd_set_voice_rate_dylibloader_wrapper_speechd
+#define spd_set_voice_rate_all spd_set_voice_rate_all_dylibloader_wrapper_speechd
+#define spd_set_voice_rate_uid spd_set_voice_rate_uid_dylibloader_wrapper_speechd
+#define spd_get_voice_rate spd_get_voice_rate_dylibloader_wrapper_speechd
+#define spd_set_voice_pitch spd_set_voice_pitch_dylibloader_wrapper_speechd
+#define spd_set_voice_pitch_all spd_set_voice_pitch_all_dylibloader_wrapper_speechd
+#define spd_set_voice_pitch_uid spd_set_voice_pitch_uid_dylibloader_wrapper_speechd
+#define spd_get_voice_pitch spd_get_voice_pitch_dylibloader_wrapper_speechd
+#define spd_set_voice_pitch_range spd_set_voice_pitch_range_dylibloader_wrapper_speechd
+#define spd_set_voice_pitch_range_all spd_set_voice_pitch_range_all_dylibloader_wrapper_speechd
+#define spd_set_voice_pitch_range_uid spd_set_voice_pitch_range_uid_dylibloader_wrapper_speechd
+#define spd_set_volume spd_set_volume_dylibloader_wrapper_speechd
+#define spd_set_volume_all spd_set_volume_all_dylibloader_wrapper_speechd
+#define spd_set_volume_uid spd_set_volume_uid_dylibloader_wrapper_speechd
+#define spd_get_volume spd_get_volume_dylibloader_wrapper_speechd
+#define spd_set_punctuation spd_set_punctuation_dylibloader_wrapper_speechd
+#define spd_set_punctuation_all spd_set_punctuation_all_dylibloader_wrapper_speechd
+#define spd_set_punctuation_uid spd_set_punctuation_uid_dylibloader_wrapper_speechd
+#define spd_set_capital_letters spd_set_capital_letters_dylibloader_wrapper_speechd
+#define spd_set_capital_letters_all spd_set_capital_letters_all_dylibloader_wrapper_speechd
+#define spd_set_capital_letters_uid spd_set_capital_letters_uid_dylibloader_wrapper_speechd
+#define spd_set_spelling spd_set_spelling_dylibloader_wrapper_speechd
+#define spd_set_spelling_all spd_set_spelling_all_dylibloader_wrapper_speechd
+#define spd_set_spelling_uid spd_set_spelling_uid_dylibloader_wrapper_speechd
+#define spd_set_language spd_set_language_dylibloader_wrapper_speechd
+#define spd_set_language_all spd_set_language_all_dylibloader_wrapper_speechd
+#define spd_set_language_uid spd_set_language_uid_dylibloader_wrapper_speechd
+#define spd_get_language spd_get_language_dylibloader_wrapper_speechd
+#define spd_set_output_module spd_set_output_module_dylibloader_wrapper_speechd
+#define spd_set_output_module_all spd_set_output_module_all_dylibloader_wrapper_speechd
+#define spd_set_output_module_uid spd_set_output_module_uid_dylibloader_wrapper_speechd
+#define spd_get_message_list_fd spd_get_message_list_fd_dylibloader_wrapper_speechd
+#define spd_list_modules spd_list_modules_dylibloader_wrapper_speechd
+#define free_spd_modules free_spd_modules_dylibloader_wrapper_speechd
+#define spd_get_output_module spd_get_output_module_dylibloader_wrapper_speechd
+#define spd_list_voices spd_list_voices_dylibloader_wrapper_speechd
+#define spd_list_synthesis_voices spd_list_synthesis_voices_dylibloader_wrapper_speechd
+#define free_spd_voices free_spd_voices_dylibloader_wrapper_speechd
+#define spd_execute_command_with_list_reply spd_execute_command_with_list_reply_dylibloader_wrapper_speechd
+#define spd_execute_command spd_execute_command_dylibloader_wrapper_speechd
+#define spd_execute_command_with_reply spd_execute_command_with_reply_dylibloader_wrapper_speechd
+#define spd_execute_command_wo_mutex spd_execute_command_wo_mutex_dylibloader_wrapper_speechd
+#define spd_send_data spd_send_data_dylibloader_wrapper_speechd
+#define spd_send_data_wo_mutex spd_send_data_wo_mutex_dylibloader_wrapper_speechd
+extern void (*SPDConnectionAddress__free_dylibloader_wrapper_speechd)( SPDConnectionAddress*);
+extern SPDConnectionAddress* (*spd_get_default_address_dylibloader_wrapper_speechd)( char**);
+extern SPDConnection* (*spd_open_dylibloader_wrapper_speechd)(const char*,const char*,const char*, SPDConnectionMode);
+extern SPDConnection* (*spd_open2_dylibloader_wrapper_speechd)(const char*,const char*,const char*, SPDConnectionMode, SPDConnectionAddress*, int, char**);
+extern void (*spd_close_dylibloader_wrapper_speechd)( SPDConnection*);
+extern int (*spd_say_dylibloader_wrapper_speechd)( SPDConnection*, SPDPriority,const char*);
+extern int (*spd_sayf_dylibloader_wrapper_speechd)( SPDConnection*, SPDPriority,const char*,...);
+extern int (*spd_stop_dylibloader_wrapper_speechd)( SPDConnection*);
+extern int (*spd_stop_all_dylibloader_wrapper_speechd)( SPDConnection*);
+extern int (*spd_stop_uid_dylibloader_wrapper_speechd)( SPDConnection*, int);
+extern int (*spd_cancel_dylibloader_wrapper_speechd)( SPDConnection*);
+extern int (*spd_cancel_all_dylibloader_wrapper_speechd)( SPDConnection*);
+extern int (*spd_cancel_uid_dylibloader_wrapper_speechd)( SPDConnection*, int);
+extern int (*spd_pause_dylibloader_wrapper_speechd)( SPDConnection*);
+extern int (*spd_pause_all_dylibloader_wrapper_speechd)( SPDConnection*);
+extern int (*spd_pause_uid_dylibloader_wrapper_speechd)( SPDConnection*, int);
+extern int (*spd_resume_dylibloader_wrapper_speechd)( SPDConnection*);
+extern int (*spd_resume_all_dylibloader_wrapper_speechd)( SPDConnection*);
+extern int (*spd_resume_uid_dylibloader_wrapper_speechd)( SPDConnection*, int);
+extern int (*spd_key_dylibloader_wrapper_speechd)( SPDConnection*, SPDPriority,const char*);
+extern int (*spd_char_dylibloader_wrapper_speechd)( SPDConnection*, SPDPriority,const char*);
+extern int (*spd_wchar_dylibloader_wrapper_speechd)( SPDConnection*, SPDPriority, wchar_t);
+extern int (*spd_sound_icon_dylibloader_wrapper_speechd)( SPDConnection*, SPDPriority,const char*);
+extern int (*spd_set_voice_type_dylibloader_wrapper_speechd)( SPDConnection*, SPDVoiceType);
+extern int (*spd_set_voice_type_all_dylibloader_wrapper_speechd)( SPDConnection*, SPDVoiceType);
+extern int (*spd_set_voice_type_uid_dylibloader_wrapper_speechd)( SPDConnection*, SPDVoiceType, unsigned int);
+extern SPDVoiceType (*spd_get_voice_type_dylibloader_wrapper_speechd)( SPDConnection*);
+extern int (*spd_set_synthesis_voice_dylibloader_wrapper_speechd)( SPDConnection*,const char*);
+extern int (*spd_set_synthesis_voice_all_dylibloader_wrapper_speechd)( SPDConnection*,const char*);
+extern int (*spd_set_synthesis_voice_uid_dylibloader_wrapper_speechd)( SPDConnection*,const char*, unsigned int);
+extern int (*spd_set_data_mode_dylibloader_wrapper_speechd)( SPDConnection*, SPDDataMode);
+extern int (*spd_set_notification_on_dylibloader_wrapper_speechd)( SPDConnection*, SPDNotification);
+extern int (*spd_set_notification_off_dylibloader_wrapper_speechd)( SPDConnection*, SPDNotification);
+extern int (*spd_set_notification_dylibloader_wrapper_speechd)( SPDConnection*, SPDNotification,const char*);
+extern int (*spd_set_voice_rate_dylibloader_wrapper_speechd)( SPDConnection*, signed int);
+extern int (*spd_set_voice_rate_all_dylibloader_wrapper_speechd)( SPDConnection*, signed int);
+extern int (*spd_set_voice_rate_uid_dylibloader_wrapper_speechd)( SPDConnection*, signed int, unsigned int);
+extern int (*spd_get_voice_rate_dylibloader_wrapper_speechd)( SPDConnection*);
+extern int (*spd_set_voice_pitch_dylibloader_wrapper_speechd)( SPDConnection*, signed int);
+extern int (*spd_set_voice_pitch_all_dylibloader_wrapper_speechd)( SPDConnection*, signed int);
+extern int (*spd_set_voice_pitch_uid_dylibloader_wrapper_speechd)( SPDConnection*, signed int, unsigned int);
+extern int (*spd_get_voice_pitch_dylibloader_wrapper_speechd)( SPDConnection*);
+extern int (*spd_set_voice_pitch_range_dylibloader_wrapper_speechd)( SPDConnection*, signed int);
+extern int (*spd_set_voice_pitch_range_all_dylibloader_wrapper_speechd)( SPDConnection*, signed int);
+extern int (*spd_set_voice_pitch_range_uid_dylibloader_wrapper_speechd)( SPDConnection*, signed int, unsigned int);
+extern int (*spd_set_volume_dylibloader_wrapper_speechd)( SPDConnection*, signed int);
+extern int (*spd_set_volume_all_dylibloader_wrapper_speechd)( SPDConnection*, signed int);
+extern int (*spd_set_volume_uid_dylibloader_wrapper_speechd)( SPDConnection*, signed int, unsigned int);
+extern int (*spd_get_volume_dylibloader_wrapper_speechd)( SPDConnection*);
+extern int (*spd_set_punctuation_dylibloader_wrapper_speechd)( SPDConnection*, SPDPunctuation);
+extern int (*spd_set_punctuation_all_dylibloader_wrapper_speechd)( SPDConnection*, SPDPunctuation);
+extern int (*spd_set_punctuation_uid_dylibloader_wrapper_speechd)( SPDConnection*, SPDPunctuation, unsigned int);
+extern int (*spd_set_capital_letters_dylibloader_wrapper_speechd)( SPDConnection*, SPDCapitalLetters);
+extern int (*spd_set_capital_letters_all_dylibloader_wrapper_speechd)( SPDConnection*, SPDCapitalLetters);
+extern int (*spd_set_capital_letters_uid_dylibloader_wrapper_speechd)( SPDConnection*, SPDCapitalLetters, unsigned int);
+extern int (*spd_set_spelling_dylibloader_wrapper_speechd)( SPDConnection*, SPDSpelling);
+extern int (*spd_set_spelling_all_dylibloader_wrapper_speechd)( SPDConnection*, SPDSpelling);
+extern int (*spd_set_spelling_uid_dylibloader_wrapper_speechd)( SPDConnection*, SPDSpelling, unsigned int);
+extern int (*spd_set_language_dylibloader_wrapper_speechd)( SPDConnection*,const char*);
+extern int (*spd_set_language_all_dylibloader_wrapper_speechd)( SPDConnection*,const char*);
+extern int (*spd_set_language_uid_dylibloader_wrapper_speechd)( SPDConnection*,const char*, unsigned int);
+extern char* (*spd_get_language_dylibloader_wrapper_speechd)( SPDConnection*);
+extern int (*spd_set_output_module_dylibloader_wrapper_speechd)( SPDConnection*,const char*);
+extern int (*spd_set_output_module_all_dylibloader_wrapper_speechd)( SPDConnection*,const char*);
+extern int (*spd_set_output_module_uid_dylibloader_wrapper_speechd)( SPDConnection*,const char*, unsigned int);
+extern int (*spd_get_message_list_fd_dylibloader_wrapper_speechd)( SPDConnection*, int, int*, char**);
+extern char** (*spd_list_modules_dylibloader_wrapper_speechd)( SPDConnection*);
+extern void (*free_spd_modules_dylibloader_wrapper_speechd)( char**);
+extern char* (*spd_get_output_module_dylibloader_wrapper_speechd)( SPDConnection*);
+extern char** (*spd_list_voices_dylibloader_wrapper_speechd)( SPDConnection*);
+extern SPDVoice** (*spd_list_synthesis_voices_dylibloader_wrapper_speechd)( SPDConnection*);
+extern void (*free_spd_voices_dylibloader_wrapper_speechd)( SPDVoice**);
+extern char** (*spd_execute_command_with_list_reply_dylibloader_wrapper_speechd)( SPDConnection*, char*);
+extern int (*spd_execute_command_dylibloader_wrapper_speechd)( SPDConnection*, char*);
+extern int (*spd_execute_command_with_reply_dylibloader_wrapper_speechd)( SPDConnection*, char*, char**);
+extern int (*spd_execute_command_wo_mutex_dylibloader_wrapper_speechd)( SPDConnection*, char*);
+extern char* (*spd_send_data_dylibloader_wrapper_speechd)( SPDConnection*,const char*, int);
+extern char* (*spd_send_data_wo_mutex_dylibloader_wrapper_speechd)( SPDConnection*,const char*, int);
+int initialize_speechd(int verbose);
+#ifdef __cplusplus
+}
+#endif
+#endif
diff --git a/platform/x11/tts_linux.cpp b/platform/x11/tts_linux.cpp
new file mode 100644
index 000000000..58bbce27e
--- /dev/null
+++ b/platform/x11/tts_linux.cpp
@@ -0,0 +1,270 @@
+/*************************************************************************/
+/* tts_linux.cpp */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#include "tts_linux.h"
+
+#include "core/project_settings.h"
+
+TTS_Linux *TTS_Linux::singleton = nullptr;
+
+static bool _is_whitespace(CharType c) {
+ return c == '\t' || c == ' ';
+}
+
+void TTS_Linux::speech_init_thread_func(void *p_userdata) {
+ TTS_Linux *tts = (TTS_Linux *)p_userdata;
+ if (tts) {
+ MutexLock thread_safe_method(tts->_thread_safe_);
+#ifdef DEBUG_ENABLED
+ int dylibloader_verbose = 1;
+#else
+ int dylibloader_verbose = 0;
+#endif
+ if (initialize_speechd(dylibloader_verbose) == 0) {
+ CharString class_str;
+ String config_name = GLOBAL_GET("application/config/name");
+ if (config_name.length() == 0) {
+ class_str = "Godot_Engine";
+ } else {
+ class_str = config_name.utf8();
+ }
+ tts->synth = spd_open(class_str, "Godot_Engine_Speech_API", "Godot_Engine", SPD_MODE_THREADED);
+ if (tts->synth) {
+ tts->synth->callback_end = &speech_event_callback;
+ tts->synth->callback_cancel = &speech_event_callback;
+ tts->synth->callback_im = &speech_event_index_mark;
+ spd_set_notification_on(tts->synth, SPD_END);
+ spd_set_notification_on(tts->synth, SPD_CANCEL);
+
+ print_verbose("Text-to-Speech: Speech Dispatcher initialized.");
+ } else {
+ print_verbose("Text-to-Speech: Cannot initialize Speech Dispatcher synthesizer!");
+ }
+ } else {
+ print_verbose("Text-to-Speech: Cannot load Speech Dispatcher library!");
+ }
+ }
+}
+
+void TTS_Linux::speech_event_index_mark(size_t p_msg_id, size_t p_client_id, SPDNotificationType p_type, char *p_index_mark) {
+ TTS_Linux *tts = TTS_Linux::get_singleton();
+ if (tts && tts->ids.has(p_msg_id)) {
+ MutexLock thread_safe_method(tts->_thread_safe_);
+ // Get word offset from the index mark injected to the text stream.
+ String mark = String::utf8(p_index_mark);
+ OS::get_singleton()->tts_post_utterance_event(OS::TTS_UTTERANCE_BOUNDARY, tts->ids[p_msg_id], mark.to_int());
+ }
+}
+
+void TTS_Linux::speech_event_callback(size_t p_msg_id, size_t p_client_id, SPDNotificationType p_type) {
+ TTS_Linux *tts = TTS_Linux::get_singleton();
+ if (tts) {
+ MutexLock thread_safe_method(tts->_thread_safe_);
+ List &queue = tts->queue;
+ if (!tts->paused && tts->ids.has(p_msg_id)) {
+ if (p_type == SPD_EVENT_END) {
+ OS::get_singleton()->tts_post_utterance_event(OS::TTS_UTTERANCE_ENDED, tts->ids[p_msg_id]);
+ tts->ids.erase(p_msg_id);
+ tts->last_msg_id = -1;
+ tts->speaking = false;
+ } else if (p_type == SPD_EVENT_CANCEL) {
+ OS::get_singleton()->tts_post_utterance_event(OS::TTS_UTTERANCE_CANCELED, tts->ids[p_msg_id]);
+ tts->ids.erase(p_msg_id);
+ tts->last_msg_id = -1;
+ tts->speaking = false;
+ }
+ }
+ if (!tts->speaking && queue.size() > 0) {
+ OS::TTSUtterance &message = queue.front()->get();
+
+ // Inject index mark after each word.
+ String text;
+ String language;
+ SPDVoice **voices = spd_list_synthesis_voices(tts->synth);
+ if (voices != nullptr) {
+ SPDVoice **voices_ptr = voices;
+ while (*voices_ptr != nullptr) {
+ if (String::utf8((*voices_ptr)->name) == message.voice) {
+ language = String::utf8((*voices_ptr)->language);
+ break;
+ }
+ voices_ptr++;
+ }
+ free_spd_voices(voices);
+ }
+ PoolIntArray breaks;
+ for (int i = 0; i < message.text.size(); i++) {
+ if (_is_whitespace(message.text[i])) {
+ breaks.push_back(i);
+ }
+ }
+ int prev = 0;
+ for (int i = 0; i < breaks.size(); i++) {
+ text += message.text.substr(prev, breaks[i] - prev);
+ text += "";
+ prev = breaks[i];
+ }
+ text += message.text.substr(prev, -1);
+
+ spd_set_synthesis_voice(tts->synth, message.voice.utf8().get_data());
+ spd_set_volume(tts->synth, message.volume * 2 - 100);
+ spd_set_voice_pitch(tts->synth, (message.pitch - 1) * 100);
+ float rate = 0;
+ if (message.rate > 1.f) {
+ rate = log10(MIN(message.rate, 2.5f)) / log10(2.5f) * 100;
+ } else if (message.rate < 1.f) {
+ rate = log10(MAX(message.rate, 0.5f)) / log10(0.5f) * -100;
+ }
+ spd_set_voice_rate(tts->synth, rate);
+ spd_set_data_mode(tts->synth, SPD_DATA_SSML);
+ tts->last_msg_id = spd_say(tts->synth, SPD_TEXT, text.utf8().get_data());
+ tts->ids[tts->last_msg_id] = message.id;
+ OS::get_singleton()->tts_post_utterance_event(OS::TTS_UTTERANCE_STARTED, message.id);
+
+ queue.pop_front();
+ tts->speaking = true;
+ }
+ }
+}
+
+bool TTS_Linux::is_speaking() const {
+ return speaking;
+}
+
+bool TTS_Linux::is_paused() const {
+ return paused;
+}
+
+Array TTS_Linux::get_voices() const {
+ _THREAD_SAFE_METHOD_
+
+ ERR_FAIL_COND_V(!synth, Array());
+ Array list;
+ SPDVoice **voices = spd_list_synthesis_voices(synth);
+ if (voices != nullptr) {
+ SPDVoice **voices_ptr = voices;
+ while (*voices_ptr != nullptr) {
+ Dictionary voice_d;
+ voice_d["name"] = String::utf8((*voices_ptr)->name);
+ voice_d["id"] = String::utf8((*voices_ptr)->name);
+ voice_d["language"] = String::utf8((*voices_ptr)->language) + "_" + String::utf8((*voices_ptr)->variant);
+ list.push_back(voice_d);
+
+ voices_ptr++;
+ }
+ free_spd_voices(voices);
+ }
+ return list;
+}
+
+void TTS_Linux::speak(const String &p_text, const String &p_voice, int p_volume, float p_pitch, float p_rate, int p_utterance_id, bool p_interrupt) {
+ _THREAD_SAFE_METHOD_
+
+ ERR_FAIL_COND(!synth);
+ if (p_interrupt) {
+ stop();
+ }
+
+ if (p_text.empty()) {
+ OS::get_singleton()->tts_post_utterance_event(OS::TTS_UTTERANCE_CANCELED, p_utterance_id);
+ return;
+ }
+
+ OS::TTSUtterance message;
+ message.text = p_text;
+ message.voice = p_voice;
+ message.volume = CLAMP(p_volume, 0, 100);
+ message.pitch = CLAMP(p_pitch, 0.f, 2.f);
+ message.rate = CLAMP(p_rate, 0.1f, 10.f);
+ message.id = p_utterance_id;
+ queue.push_back(message);
+
+ if (is_paused()) {
+ resume();
+ } else {
+ speech_event_callback(0, 0, SPD_EVENT_BEGIN);
+ }
+}
+
+void TTS_Linux::pause() {
+ _THREAD_SAFE_METHOD_
+
+ ERR_FAIL_COND(!synth);
+ if (spd_pause(synth) == 0) {
+ paused = true;
+ }
+}
+
+void TTS_Linux::resume() {
+ _THREAD_SAFE_METHOD_
+
+ ERR_FAIL_COND(!synth);
+ spd_resume(synth);
+ paused = false;
+}
+
+void TTS_Linux::stop() {
+ _THREAD_SAFE_METHOD_
+
+ ERR_FAIL_COND(!synth);
+ for (List::Element *E = queue.front(); E; E = E->next()) {
+ OS::TTSUtterance &message = E->get();
+ OS::get_singleton()->tts_post_utterance_event(OS::TTS_UTTERANCE_CANCELED, message.id);
+ }
+ if ((last_msg_id != -1) && ids.has(last_msg_id)) {
+ OS::get_singleton()->tts_post_utterance_event(OS::TTS_UTTERANCE_CANCELED, ids[last_msg_id]);
+ }
+ queue.clear();
+ ids.clear();
+ last_msg_id = -1;
+ spd_cancel(synth);
+ spd_resume(synth);
+ speaking = false;
+ paused = false;
+}
+
+TTS_Linux *TTS_Linux::get_singleton() {
+ return singleton;
+}
+
+TTS_Linux::TTS_Linux() {
+ singleton = this;
+ // Speech Dispatcher init can be slow, it might wait for helper process to start on background, so run it in the thread.
+ init_thread.start(speech_init_thread_func, this);
+}
+
+TTS_Linux::~TTS_Linux() {
+ init_thread.wait_to_finish();
+ if (synth) {
+ spd_close(synth);
+ }
+
+ singleton = nullptr;
+}
diff --git a/platform/x11/tts_linux.h b/platform/x11/tts_linux.h
new file mode 100644
index 000000000..ac3da2056
--- /dev/null
+++ b/platform/x11/tts_linux.h
@@ -0,0 +1,78 @@
+/*************************************************************************/
+/* tts_linux.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef TTS_LINUX_H
+#define TTS_LINUX_H
+
+#include "core/array.h"
+#include "core/list.h"
+#include "core/map.h"
+#include "core/os/os.h"
+#include "core/os/thread.h"
+#include "core/os/thread_safe.h"
+#include "core/ustring.h"
+
+#include "speechd-so_wrap.h"
+
+class TTS_Linux {
+ _THREAD_SAFE_CLASS_
+
+ List queue;
+ SPDConnection *synth = nullptr;
+ bool speaking = false;
+ bool paused = false;
+ int last_msg_id = -1;
+ HashMap ids;
+
+ Thread init_thread;
+
+ static void speech_init_thread_func(void *p_userdata);
+ static void speech_event_callback(size_t p_msg_id, size_t p_client_id, SPDNotificationType p_type);
+ static void speech_event_index_mark(size_t p_msg_id, size_t p_client_id, SPDNotificationType p_type, char *p_index_mark);
+
+ static TTS_Linux *singleton;
+
+public:
+ static TTS_Linux *get_singleton();
+
+ bool is_speaking() const;
+ bool is_paused() const;
+ Array get_voices() const;
+
+ void speak(const String &p_text, const String &p_voice, int p_volume = 50, float p_pitch = 1.f, float p_rate = 1.f, int p_utterance_id = 0, bool p_interrupt = false);
+ void pause();
+ void resume();
+ void stop();
+
+ TTS_Linux();
+ ~TTS_Linux();
+};
+
+#endif // TTS_LINUX_H