1
0
mirror of https://github.com/godotengine/godot.git synced 2025-11-06 12:20:30 +00:00

Implement text-to-speech support on Android, iOS, HTML5, Linux, macOS and Windows.

Implement TextServer word break method.
This commit is contained in:
bruvzg
2021-11-04 14:33:37 +02:00
parent 3e1b824c05
commit 6ab672d1ef
54 changed files with 3962 additions and 2 deletions

View File

@@ -36,6 +36,7 @@ import static android.content.Context.WINDOW_SERVICE;
import org.godotengine.godot.input.GodotEditText;
import org.godotengine.godot.plugin.GodotPlugin;
import org.godotengine.godot.plugin.GodotPluginRegistry;
import org.godotengine.godot.tts.GodotTTS;
import org.godotengine.godot.utils.GodotNetUtils;
import org.godotengine.godot.utils.PermissionsUtil;
import org.godotengine.godot.xr.XRMode;
@@ -165,6 +166,7 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC
public static GodotIO io;
public static GodotNetUtils netUtils;
public static GodotTTS tts;
public interface ResultCallback {
void callback(int requestCode, int resultCode, Intent data);
@@ -458,6 +460,7 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC
io = new GodotIO(activity);
GodotLib.io = io;
netUtils = new GodotNetUtils(activity);
tts = new GodotTTS(activity);
mSensorManager = (SensorManager)activity.getSystemService(Context.SENSOR_SERVICE);
mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
mSensorManager.registerListener(this, mAccelerometer, SensorManager.SENSOR_DELAY_GAME);

View File

@@ -91,6 +91,11 @@ public class GodotLib {
*/
public static native boolean step();
/**
* TTS callback.
*/
public static native void ttsCallback(int event, int id, int pos);
/**
* Forward touch events from the main thread to the GL thread.
*/

View File

@@ -0,0 +1,298 @@
/*************************************************************************/
/* GodotTTS.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.godotengine.godot.tts;
import org.godotengine.godot.GodotLib;
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.
* <p>
* A [GodotTTS] provides the following features:
* <p>
* <ul>
* <li>Access to the Android Text to Speech API.
* <li>Utterance pause / resume functions, unsupported by Android TTS API.
* </ul>
*/
@Keep
public class GodotTTS extends UtteranceProgressListener {
// Note: These constants must be in sync with DisplayServer::TTSUtteranceEvent enum from "servers/display_server.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<GodotUtterance> queue;
final private Object lock = new Object();
private GodotUtterance lastUtterance;
private boolean speaking;
private boolean paused;
public GodotTTS(Activity p_activity) {
synth = new TextToSpeech(p_activity, null);
queue = new LinkedList<GodotUtterance>();
synth.setOnUtteranceProgressListener(this);
}
private void updateTTS() {
if (!speaking && queue.size() > 0) {
int mode = TextToSpeech.QUEUE_FLUSH;
GodotUtterance message = queue.pollFirst();
Set<Voice> 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;
GodotLib.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) {
GodotLib.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) {
GodotLib.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) {
GodotLib.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) {
GodotLib.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) {
GodotLib.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) {
GodotUtterance message = new GodotUtterance(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<Voice> 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 (GodotUtterance u : queue) {
GodotLib.ttsCallback(EVENT_CANCEL, u.id, 0);
}
queue.clear();
if (lastUtterance != null) {
GodotLib.ttsCallback(EVENT_CANCEL, lastUtterance.id, 0);
}
lastUtterance = null;
paused = false;
speaking = false;
synth.stop();
}
}
/**
* Returns voice information.
*/
public String[] getVoices() {
Set<Voice> 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;
}
}

View File

@@ -0,0 +1,55 @@
/*************************************************************************/
/* GodotUtterance.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.godotengine.godot.tts;
/**
* A speech request for GodotTTS.
*/
class GodotUtterance {
final String text;
final String voice;
final int volume;
final float pitch;
final float rate;
final int id;
int offset = -1;
int start = 0;
GodotUtterance(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;
}
}