| @@ -1,6 +1,8 @@ | |||
| LOCAL_PATH:= $(call my-dir) | |||
| include $(CLEAR_VARS) | |||
| LOCAL_CFLAGS = -std=c++11 | |||
| # ucd-tools wide-character compatibility support: | |||
| UCDTOOLS_SRC_PATH := ../../ucd-tools/src | |||
| @@ -31,6 +31,77 @@ | |||
| #include <TtsEngine.h> | |||
| #include <Log.h> | |||
| /** @name Java to Wide String Helpers | |||
| * @brief These are helpers for converting a jstring to wchar_t*. | |||
| * | |||
| * This assumes that wchar_t is a 32-bit (UTF-32) value. | |||
| */ | |||
| //@{ | |||
| const char *utf8_read(const char *in, wchar_t &c) | |||
| { | |||
| if (uint8_t(*in) < 0x80) | |||
| c = *in++; | |||
| else switch (uint8_t(*in) & 0xF0) | |||
| { | |||
| default: | |||
| c = uint8_t(*in++) & 0x1F; | |||
| c = (c << 6) + (uint8_t(*in++) & 0x3F); | |||
| break; | |||
| case 0xE0: | |||
| c = uint8_t(*in++) & 0x0F; | |||
| c = (c << 6) + (uint8_t(*in++) & 0x3F); | |||
| c = (c << 6) + (uint8_t(*in++) & 0x3F); | |||
| break; | |||
| case 0xF0: | |||
| c = uint8_t(*in++) & 0x07; | |||
| c = (c << 6) + (uint8_t(*in++) & 0x3F); | |||
| c = (c << 6) + (uint8_t(*in++) & 0x3F); | |||
| c = (c << 6) + (uint8_t(*in++) & 0x3F); | |||
| break; | |||
| } | |||
| return in; | |||
| } | |||
| class unicode_string | |||
| { | |||
| static_assert(sizeof(wchar_t) == 4, "wchar_t is not UTF-32"); | |||
| public: | |||
| unicode_string(JNIEnv *env, jstring str); | |||
| ~unicode_string(); | |||
| const wchar_t *c_str() const { return mString; } | |||
| private: | |||
| wchar_t *mString; | |||
| }; | |||
| unicode_string::unicode_string(JNIEnv *env, jstring str) | |||
| : mString(NULL) | |||
| { | |||
| if (str == NULL) return; | |||
| const char *utf8 = env->GetStringUTFChars(str, NULL); | |||
| mString = (wchar_t *)malloc(strlen(utf8) + 1); | |||
| const char *utf8_current = utf8; | |||
| wchar_t *utf32_current = mString; | |||
| while (*utf8_current) | |||
| { | |||
| utf8_current = utf8_read(utf8_current, *utf32_current); | |||
| ++utf32_current; | |||
| } | |||
| *utf32_current = 0; | |||
| env->ReleaseStringUTFChars(str, utf8); | |||
| } | |||
| unicode_string::~unicode_string() | |||
| { | |||
| if (mString) free(mString); | |||
| } | |||
| //@} | |||
| #define LOG_TAG "eSpeakService" | |||
| #define DEBUG true | |||
| @@ -314,6 +385,23 @@ JNICALL Java_com_reecedunn_espeak_SpeechSynthesis_nativeGetParameter( | |||
| return espeak_GetParameter((espeak_PARAMETER)parameter, (int)current); | |||
| } | |||
| JNIEXPORT jboolean | |||
| JNICALL Java_com_reecedunn_espeak_SpeechSynthesis_nativeSetPunctuationCharacters( | |||
| JNIEnv *env, jobject object, jstring characters) { | |||
| if (DEBUG) LOGV("%s)", __FUNCTION__); | |||
| unicode_string list(env, characters); | |||
| const espeak_ERROR result = espeak_SetPunctuationList(list.c_str()); | |||
| switch (result) { | |||
| case EE_OK: return JNI_TRUE; | |||
| case EE_INTERNAL_ERROR: LOGE("espeak_SetPunctuationList: internal error."); break; | |||
| case EE_BUFFER_FULL: LOGE("espeak_SetPunctuationList: buffer full."); break; | |||
| case EE_NOT_FOUND: LOGE("espeak_SetPunctuationList: not found."); break; | |||
| } | |||
| return JNI_FALSE; | |||
| } | |||
| JNIEXPORT jboolean | |||
| JNICALL Java_com_reecedunn_espeak_SpeechSynthesis_nativeSynthesize( | |||
| JNIEnv *env, jobject object, jstring text, jboolean isSsml) { | |||
| @@ -0,0 +1,39 @@ | |||
| <?xml version="1.0" encoding="utf-8"?> | |||
| <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" | |||
| android:layout_width="match_parent" | |||
| android:layout_height="match_parent" | |||
| android:orientation="vertical" > | |||
| <RadioGroup | |||
| android:layout_width="fill_parent" | |||
| android:layout_height="wrap_content" | |||
| android:focusable="false"> | |||
| <RadioButton | |||
| android:layout_width="match_parent" | |||
| android:layout_height="48dp" | |||
| android:text="@string/punctuation_all" | |||
| android:id="@+id/all" | |||
| android:focusable="false" | |||
| android:focusableInTouchMode="false" | |||
| android:clickable="true" | |||
| android:checked="false" /> | |||
| <RadioButton | |||
| android:layout_width="match_parent" | |||
| android:layout_height="48dp" | |||
| android:text="@string/punctuation_custom" | |||
| android:id="@+id/custom" | |||
| android:focusable="false" | |||
| android:focusableInTouchMode="false" | |||
| android:clickable="true" /> | |||
| </RadioGroup> | |||
| <EditText | |||
| android:layout_width="fill_parent" | |||
| android:layout_height="wrap_content" | |||
| android:id="@+id/punctuation_characters" | |||
| android:hint="@string/punctuation_characters" | |||
| android:layout_marginLeft="32dp" /> | |||
| </LinearLayout> | |||
| @@ -34,10 +34,4 @@ | |||
| <item>female-old</item> | |||
| </string-array> | |||
| <string-array name="punctuation_level_values"> | |||
| <item>0</item> | |||
| <item>1</item> | |||
| <item>2</item> | |||
| </string-array> | |||
| </resources> | |||
| @@ -111,14 +111,10 @@ | |||
| <string name="resetToDefault">Set to default</string> | |||
| <string name="espeak_pitch_range">Pitch variation</string> | |||
| <string name="espeak_variant">Voice variant</string> | |||
| <string name="espeak_punctuation_level">Punctuation level</string> | |||
| <!-- | |||
| Source: Punctuation level preference labels. | |||
| Description: Labels for possible punctuation level values. | |||
| --> | |||
| <string-array name="punctuation_level_entries"> | |||
| <item>None</item> | |||
| <item>All</item> | |||
| <item>Some</item> | |||
| </string-array> | |||
| <string name="espeak_speak_punctuation">Speak punctuation</string> | |||
| <string name="punctuation_all">All</string> | |||
| <string name="punctuation_custom">Custom</string> | |||
| <string name="punctuation_custom_fmt">Custom: %s</string> | |||
| <string name="punctuation_none">None</string> | |||
| <string name="punctuation_characters">Punctuation characters</string> | |||
| </resources> | |||
| @@ -9,12 +9,4 @@ | |||
| android:summary="%s" | |||
| android:title="@string/espeak_variant" /> | |||
| <ListPreference | |||
| android:defaultValue="0" | |||
| android:entries="@array/punctuation_level_entries" | |||
| android:entryValues="@array/punctuation_level_values" | |||
| android:key="espeak_punctuation_level" | |||
| android:summary="%s" | |||
| android:title="@string/espeak_punctuation_level" /> | |||
| </PreferenceScreen> | |||
| @@ -0,0 +1,129 @@ | |||
| /* | |||
| * Copyright (C) 2013 Reece H. Dunn | |||
| * | |||
| * Licensed under the Apache License, Version 2.0 (the "License"); | |||
| * you may not use this file except in compliance with the License. | |||
| * You may obtain a copy of the License at | |||
| * | |||
| * http://www.apache.org/licenses/LICENSE-2.0 | |||
| * | |||
| * Unless required by applicable law or agreed to in writing, software | |||
| * distributed under the License is distributed on an "AS IS" BASIS, | |||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||
| * See the License for the specific language governing permissions and | |||
| * limitations under the License. | |||
| */ | |||
| package com.reecedunn.espeak; | |||
| import android.content.Context; | |||
| import android.content.DialogInterface; | |||
| import android.content.SharedPreferences; | |||
| import android.preference.DialogPreference; | |||
| import android.text.Editable; | |||
| import android.util.AttributeSet; | |||
| import android.view.View; | |||
| import android.widget.Button; | |||
| import android.widget.EditText; | |||
| import android.widget.RadioButton; | |||
| import android.widget.SeekBar; | |||
| import android.widget.TextView; | |||
| public class SpeakPunctuationPreference extends DialogPreference { | |||
| private RadioButton mAll; | |||
| private RadioButton mCustom; | |||
| private EditText mPunctuationCharacters; | |||
| private VoiceSettings mSettings; | |||
| public SpeakPunctuationPreference(Context context, AttributeSet attrs, int defStyle) { | |||
| super(context, attrs, defStyle); | |||
| setDialogLayoutResource(R.layout.speak_punctuation_preference); | |||
| setPositiveButtonText(android.R.string.ok); | |||
| setNegativeButtonText(android.R.string.cancel); | |||
| } | |||
| public SpeakPunctuationPreference(Context context, AttributeSet attrs) { | |||
| this(context, attrs, 0); | |||
| } | |||
| public SpeakPunctuationPreference(Context context) { | |||
| this(context, null); | |||
| } | |||
| public void setVoiceSettings(VoiceSettings settings) { | |||
| mSettings = settings; | |||
| onDataChanged(mSettings.getPunctuationLevel(), mSettings.getPunctuationCharacters()); | |||
| } | |||
| private void onDataChanged(int level, String characters) { | |||
| switch (level) { | |||
| case SpeechSynthesis.PUNCT_ALL: | |||
| callChangeListener(getContext().getText(R.string.punctuation_all)); | |||
| break; | |||
| case SpeechSynthesis.PUNCT_SOME: | |||
| if (characters == null || characters.isEmpty()) { | |||
| callChangeListener(getContext().getText(R.string.punctuation_none)); | |||
| } else { | |||
| callChangeListener(String.format(getContext().getText(R.string.punctuation_custom_fmt).toString(), characters)); | |||
| } | |||
| break; | |||
| case SpeechSynthesis.PUNCT_NONE: | |||
| callChangeListener(getContext().getText(R.string.punctuation_none)); | |||
| break; | |||
| } | |||
| } | |||
| @Override | |||
| protected View onCreateDialogView() { | |||
| View root = super.onCreateDialogView(); | |||
| mAll = (RadioButton)root.findViewById(R.id.all); | |||
| mCustom = (RadioButton)root.findViewById(R.id.custom); | |||
| mPunctuationCharacters = (EditText)root.findViewById(R.id.punctuation_characters); | |||
| return root; | |||
| } | |||
| @Override | |||
| protected void onBindDialogView(View view) { | |||
| super.onBindDialogView(view); | |||
| if (mSettings.getPunctuationLevel() == SpeechSynthesis.PUNCT_ALL) { | |||
| mAll.toggle(); | |||
| } else { | |||
| mCustom.toggle(); | |||
| } | |||
| mPunctuationCharacters.setText(mSettings.getPunctuationCharacters()); | |||
| } | |||
| @Override | |||
| public void onClick(DialogInterface dialog, int which) { | |||
| switch (which) { | |||
| case DialogInterface.BUTTON_POSITIVE: | |||
| Editable text = mPunctuationCharacters.getText(); | |||
| String characters = null; | |||
| int level; | |||
| if (text != null) { | |||
| characters = text.toString(); | |||
| } | |||
| if (characters == null || characters.isEmpty()) { | |||
| level = mAll.isChecked() ? SpeechSynthesis.PUNCT_ALL : SpeechSynthesis.PUNCT_NONE; | |||
| } else { | |||
| level = mAll.isChecked() ? SpeechSynthesis.PUNCT_ALL : SpeechSynthesis.PUNCT_SOME; | |||
| } | |||
| onDataChanged(level, characters); | |||
| if (shouldCommit()) { | |||
| SharedPreferences.Editor editor = getEditor(); | |||
| if (editor != null) { | |||
| editor.putString(VoiceSettings.PREF_PUNCTUATION_CHARACTERS, characters); | |||
| editor.putString(VoiceSettings.PREF_PUNCTUATION_LEVEL, Integer.toString(level)); | |||
| editor.commit(); | |||
| } | |||
| } | |||
| break; | |||
| } | |||
| super.onClick(dialog, which); | |||
| } | |||
| } | |||
| @@ -193,14 +193,18 @@ public class SpeechSynthesis { | |||
| } | |||
| } | |||
| public void setPunctuationCharacters(String characters) { | |||
| nativeSetPunctuationCharacters(characters); | |||
| } | |||
| /** Don't announce any punctuation characters. */ | |||
| public static int PUNCT_NONE = 0; | |||
| public static final int PUNCT_NONE = 0; | |||
| /** Announce every punctuation character. */ | |||
| public static int PUNCT_ALL = 1; | |||
| public static final int PUNCT_ALL = 1; | |||
| /** Announce some of the punctuation characters. */ | |||
| public static int PUNCT_SOME = 2; | |||
| public static final int PUNCT_SOME = 2; | |||
| public enum UnitType { | |||
| Percentage, | |||
| @@ -345,6 +349,8 @@ public class SpeechSynthesis { | |||
| private native final int nativeGetParameter(int parameter, int current); | |||
| private native final boolean nativeSetPunctuationCharacters(String characters); | |||
| private native final boolean nativeSynthesize(String text, boolean isSsml); | |||
| private native final boolean nativeStop(); | |||
| @@ -241,6 +241,7 @@ public class TtsService extends TextToSpeechService { | |||
| mEngine.PitchRange.setValue(settings.getPitchRange()); | |||
| mEngine.Volume.setValue(settings.getVolume()); | |||
| mEngine.Punctuation.setValue(settings.getPunctuationLevel()); | |||
| mEngine.setPunctuationCharacters(settings.getPunctuationCharacters()); | |||
| mEngine.synthesize(text, text.startsWith("<speak")); | |||
| } | |||
| @@ -97,6 +97,18 @@ public class TtsSettingsActivity extends PreferenceActivity { | |||
| } | |||
| } | |||
| private static Preference createSpeakPunctuationPreference(Context context, SpeechSynthesis engine, int titleRes) { | |||
| final String title = context.getString(titleRes); | |||
| final SpeakPunctuationPreference pref = new SpeakPunctuationPreference(context); | |||
| pref.setTitle(title); | |||
| pref.setDialogTitle(title); | |||
| pref.setOnPreferenceChangeListener(mOnPreferenceChanged); | |||
| pref.setPersistent(true); | |||
| pref.setVoiceSettings(new VoiceSettings(PreferenceManager.getDefaultSharedPreferences(context), engine)); | |||
| return pref; | |||
| } | |||
| private static Preference createPreference(Context context, SpeechSynthesis.Parameter parameter, String key, int titleRes) { | |||
| final String title = context.getString(titleRes); | |||
| final int defaultValue = parameter.getDefaultValue(); | |||
| @@ -165,6 +177,7 @@ public class TtsSettingsActivity extends PreferenceActivity { | |||
| SpeechSynthesis engine = new SpeechSynthesis(context, null); | |||
| group.addPreference(createSpeakPunctuationPreference(context, engine, R.string.espeak_speak_punctuation)); | |||
| group.addPreference(createPreference(context, engine.Rate, "espeak_rate", R.string.setting_default_rate)); | |||
| group.addPreference(createPreference(context, engine.Pitch, "espeak_pitch", R.string.setting_default_pitch)); | |||
| group.addPreference(createPreference(context, engine.PitchRange, "espeak_pitch_range", R.string.espeak_pitch_range)); | |||
| @@ -189,6 +202,8 @@ public class TtsSettingsActivity extends PreferenceActivity { | |||
| final SeekBarPreference seekBarPreference = (SeekBarPreference) preference; | |||
| String formatter = seekBarPreference.getFormatter(); | |||
| summary = String.format(formatter, (String)newValue); | |||
| } else { | |||
| summary = (String)newValue; | |||
| } | |||
| preference.setSummary(summary); | |||
| } | |||