/**************************************************************************/ /* GodotTTS.java */ /**************************************************************************/ /* This file is part of: */ /* REDOT ENGINE */ /* https://redotengine.org */ /**************************************************************************/ /* Copyright (c) 2024-present Redot Engine contributors */ /* (see REDOT_AUTHORS.md) */ /* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ /* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ /* */ /* 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.content.Context; 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 [GodotTTS] provides the following features: *

*

*/ @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; private final Context context; private TextToSpeech synth; private LinkedList queue; final private Object lock = new Object(); private GodotUtterance lastUtterance; private boolean speaking; private boolean paused; public GodotTTS(Context context) { this.context = context; } private void updateTTS() { if (!speaking && queue.size() > 0) { int mode = TextToSpeech.QUEUE_FLUSH; GodotUtterance 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; 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(); } } } /** * Initialize synth and query. */ public void init() { synth = new TextToSpeech(context, null); queue = new LinkedList(); synth.setOnUtteranceProgressListener(this); } /** * 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 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 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; } }