Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New JavaScript api for TTS #8812

Merged
merged 17 commits into from
Aug 27, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4014,6 +4014,8 @@ public JavaScriptFunction javaScriptFunction() {
}

public class JavaScriptFunction {
// Text to speech
private JavaScriptTTS mTalker = new JavaScriptTTS();

// if supplied api version match then enable api
private void enableJsApi() {
Expand Down Expand Up @@ -4247,5 +4249,51 @@ public boolean ankiIsActiveNetworkMetered() {
return true;
}
}

@JavascriptInterface
public int ankiTtsSpeak(String text, int queueMode) {
return mTalker.speak(text, queueMode);
}

@JavascriptInterface
public int ankiTtsSpeak(String text) {
return mTalker.speak(text);
}

@JavascriptInterface
public int ankiTtsSetLanguage(String loc) {
return mTalker.setLanguage(loc);
}

@JavascriptInterface
public int ankiTtsSetPitch(float pitch) {
return mTalker.setPitch(pitch);
}

@JavascriptInterface
public int ankiTtsSetPitch(double pitch) {
return mTalker.setPitch((float)pitch);
}

@JavascriptInterface
public int ankiTtsSetSpeechRate(float speechRate) {
return mTalker.setSpeechRate(speechRate);
}

@JavascriptInterface
public int ankiTtsSetSpeechRate(double speechRate) {
return mTalker.setSpeechRate((float)speechRate);
}

@JavascriptInterface
public boolean ankiTtsIsSpeaking() {
return mTalker.isSpeaking();
}

@JavascriptInterface
public int ankiTtsStop() {
return mTalker.stop();
}

}
}
150 changes: 150 additions & 0 deletions AnkiDroid/src/main/java/com/ichi2/anki/JavaScriptTTS.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/****************************************************************************************
* Copyright (c) 2021 mikunimaru <[email protected]> *
*
* This program is free software; you can redistribute it and/or modify it under *
* the terms of the GNU General Public License as published by the Free Software *
* Foundation; either version 3 of the License, or (at your option) any later *
* version. *
* *
* This program is distributed in the hope that it will be useful, but WITHOUT ANY *
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A *
* PARTICULAR PURPOSE. See the GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License along with *
* this program. If not, see <http://www.gnu.org/licenses/>. *
****************************************************************************************/

package com.ichi2.anki;
david-allison marked this conversation as resolved.
Show resolved Hide resolved

import android.content.Context;
import android.os.Bundle;
import android.speech.tts.TextToSpeech;

import androidx.annotation.IntDef;
import androidx.annotation.NonNull;

/**
* Since it is assumed that only advanced users will use the JavaScript api,
* here, Android's TextToSpeech is converted for JavaScript almost as it is, giving priority to free behavior.
* https://developer.android.com/reference/android/speech/tts/TextToSpeech
*/
public class JavaScriptTTS implements TextToSpeech.OnInitListener {

private static final int TTS_SUCCESS = TextToSpeech.SUCCESS;
private static final int TTS_ERROR = TextToSpeech.ERROR;
private static final int TTS_QUEUE_ADD = TextToSpeech.QUEUE_ADD;
private static final int TTS_QUEUE_FLUSH = TextToSpeech.QUEUE_FLUSH;
private static final int TTS_LANG_AVAILABLE = TextToSpeech.LANG_AVAILABLE;
private static final int TTS_LANG_COUNTRY_AVAILABLE = TextToSpeech.LANG_COUNTRY_AVAILABLE;
private static final int TTS_LANG_COUNTRY_VAR_AVAILABLE = TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE;
private static final int TTS_LANG_MISSING_DATA = TextToSpeech.LANG_MISSING_DATA;
private static final int TTS_LANG_NOT_SUPPORTED = TextToSpeech.LANG_NOT_SUPPORTED;

@IntDef({TTS_SUCCESS, TTS_ERROR})
public @interface ErrorOrSuccess {}

@IntDef({TTS_QUEUE_ADD, TTS_QUEUE_FLUSH})
public @interface QueueMode {}

@IntDef({TTS_LANG_AVAILABLE, TTS_LANG_COUNTRY_AVAILABLE, TTS_LANG_COUNTRY_VAR_AVAILABLE, TTS_LANG_MISSING_DATA, TTS_LANG_NOT_SUPPORTED})
public @interface TTSLangResult {}

@NonNull
private static TextToSpeech mTts;
private static boolean mTtsOk;
private static final Bundle mTtsParams = new Bundle();

JavaScriptTTS() {
Context mContext = AnkiDroidApp.getInstance().getApplicationContext();
mTts = new TextToSpeech(mContext, this);
}

/** OnInitListener method to receive the TTS engine status */
@Override
public void onInit(@ErrorOrSuccess int status) {
mTtsOk = status == TextToSpeech.SUCCESS;
}

/**
* A method to speak something
* @param text Content to speak
* @param queueMode 1 for QUEUE_ADD and 0 for QUEUE_FLUSH.
* @return ERROR(-1) SUCCESS(0)
*/
@ErrorOrSuccess
public int speak(String text, @QueueMode int queueMode) {
return mTts.speak(text, queueMode, mTtsParams, "stringId");
}

/**
* If only a string is given, set QUEUE_FLUSH to the default behavior.
* @param text Content to speak
* @return ERROR(-1) SUCCESS(0)
*/
@ErrorOrSuccess
public int speak(String text) {
return mTts.speak(text, TextToSpeech.QUEUE_FLUSH, mTtsParams, "stringId");
}

/**
* Sets the text-to-speech language.
* The TTS engine will try to use the closest match to the specified language as represented by the Locale, but there is no guarantee that the exact same Locale will be used.
* @param loc Specifying the language to speak
* @return 0 Denotes the language is available for the language by the locale, but not the country and variant.
* <li> 1 Denotes the language is available for the language and country specified by the locale, but not the variant.
* <li> 2 Denotes the language is available exactly as specified by the locale.
* <li> -1 Denotes the language data is missing.
* <li> -2 Denotes the language is not supported.
*/
@TTSLangResult
public int setLanguage(String loc) {
// The Int values will be returned
// Code indicating the support status for the locale. See LANG_AVAILABLE, LANG_COUNTRY_AVAILABLE, LANG_COUNTRY_VAR_AVAILABLE, LANG_MISSING_DATA and LANG_NOT_SUPPORTED.
return mTts.setLanguage(LanguageUtils.localeFromStringIgnoringScriptAndExtensions(loc));
}


/**
* Sets the speech pitch for the TextToSpeech engine. This has no effect on any pre-recorded speech.
* @param pitch float: Speech pitch. 1.0 is the normal pitch, lower values lower the tone of the synthesized voice, greater values increase it.
* @return ERROR(-1) SUCCESS(0)
*/
@ErrorOrSuccess
public int setPitch(float pitch) {
// The following Int values will be returned
// ERROR(-1) SUCCESS(0)
return mTts.setPitch(pitch);
}

/**
*
* @param speechRate Sets the speech rate. 1.0 is the normal speech rate. This has no effect on any pre-recorded speech.
* @return ERROR(-1) SUCCESS(0)
*/
@ErrorOrSuccess
public int setSpeechRate(float speechRate) {
// The following Int values will be returned
// ERROR(-1) SUCCESS(0)
return mTts.setSpeechRate(speechRate);
}

/**
* Checks whether the TTS engine is busy speaking.
* Note that a speech item is considered complete once it's audio data has
* been sent to the audio mixer, or written to a file.
*
*/
public boolean isSpeaking() {
return mTts.isSpeaking();
}

/**
* Interrupts the current utterance (whether played or rendered to file) and discards other utterances in the queue.
* @return ERROR(-1) SUCCESS(0)
*/
@ErrorOrSuccess
public int stop() {
return mTts.stop();
}

}
58 changes: 58 additions & 0 deletions AnkiDroid/src/main/java/com/ichi2/anki/LanguageUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/****************************************************************************************
* Copyright (c) 2021 mikunimaru <[email protected]> *
*
* This program is free software; you can redistribute it and/or modify it under *
* the terms of the GNU General Public License as published by the Free Software *
* Foundation; either version 3 of the License, or (at your option) any later *
* version. *
* *
* This program is distributed in the hope that it will be useful, but WITHOUT ANY *
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A *
* PARTICULAR PURPOSE. See the GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License along with *
* this program. If not, see <http://www.gnu.org/licenses/>. *
****************************************************************************************/

package com.ichi2.anki;
david-allison marked this conversation as resolved.
Show resolved Hide resolved

import java.util.Locale;

public class LanguageUtils {

/**
* Convert a string representation of a locale, in the format returned by Locale.toString(),
* into a Locale object, disregarding any script and extensions fields (i.e. using solely the
* language, country and variant fields).
* <p>
* Returns a Locale object constructed from an empty string if the input string is null, empty
* or contains more than 3 fields separated by underscores.
*/
public static Locale localeFromStringIgnoringScriptAndExtensions(String localeCode) {
if (localeCode == null) {
return new Locale("");
}

localeCode = stripScriptAndExtensions(localeCode);

String[] fields = localeCode.split("_");
switch (fields.length) {
case 1:
return new Locale(fields[0]);
case 2:
return new Locale(fields[0], fields[1]);
case 3:
return new Locale(fields[0], fields[1], fields[2]);
default:
return new Locale("");
}
}

private static String stripScriptAndExtensions(String localeCode) {
int hashPos = localeCode.indexOf('#');
if (hashPos >= 0) {
localeCode = localeCode.substring(0, hashPos);
}
return localeCode;
}
}
40 changes: 2 additions & 38 deletions AnkiDroid/src/main/java/com/ichi2/anki/ReadText.java
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public static Sound.SoundSide getmQuestionAnswer() {
}

public static void speak(String text, String loc, int queueMode) {
int result = mTts.setLanguage(localeFromStringIgnoringScriptAndExtensions(loc));
int result = mTts.setLanguage(LanguageUtils.localeFromStringIgnoringScriptAndExtensions(loc));
if (result == TextToSpeech.LANG_MISSING_DATA || result == TextToSpeech.LANG_NOT_SUPPORTED) {
UIUtils.showThemedToast(mReviewer.get(), mReviewer.get().getString(R.string.no_tts_available_message)
+ " (" + loc + ")", false);
Expand Down Expand Up @@ -223,48 +223,12 @@ private static void textToSpeech(String text, long did, int ord, Sound.SoundSide
selectTts(mTextToSpeak, mDid, mOrd, mQuestionAnswer);
}

/**
* Convert a string representation of a locale, in the format returned by Locale.toString(),
* into a Locale object, disregarding any script and extensions fields (i.e. using solely the
* language, country and variant fields).
* <p>
* Returns a Locale object constructed from an empty string if the input string is null, empty
* or contains more than 3 fields separated by underscores.
*/
private static Locale localeFromStringIgnoringScriptAndExtensions(String localeCode) {
if (localeCode == null) {
return new Locale("");
}

localeCode = stripScriptAndExtensions(localeCode);

String[] fields = localeCode.split("_");
switch (fields.length) {
case 1:
return new Locale(fields[0]);
case 2:
return new Locale(fields[0], fields[1]);
case 3:
return new Locale(fields[0], fields[1], fields[2]);
default:
return new Locale("");
}
}

private static String stripScriptAndExtensions(String localeCode) {
int hashPos = localeCode.indexOf('#');
if (hashPos >= 0) {
localeCode = localeCode.substring(0, hashPos);
}
return localeCode;
}

/**
* Returns true if the TTS engine supports the language of the locale represented by localeCode
* (which should be in the format returned by Locale.toString()), false otherwise.
*/
private static boolean isLanguageAvailable(String localeCode) {
return mTts.isLanguageAvailable(localeFromStringIgnoringScriptAndExtensions(localeCode)) >=
return mTts.isLanguageAvailable(LanguageUtils.localeFromStringIgnoringScriptAndExtensions(localeCode)) >=
TextToSpeech.LANG_AVAILABLE;
}

Expand Down