diff --git a/.gitignore b/.gitignore index 8d0f9084..2c1fc894 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,8 @@ *~ .*.swp -#Eclipse project files +# Eclipse project files: + .cproject .project @@ -12,6 +13,27 @@ *.o *.lo +# android build output: + +bin/ +obj/ +gen/ +android/icons/*.png + +libttsespeak.so + +android/res/raw/espeakdata.zip +android/res/raw/espeakdata_version + +local.properties + +# android gradle output: + +.gradle/ +.idea/ +build/ +espeak.iml + # autotools AUTHORS diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000..b080d2dd --- /dev/null +++ b/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,4 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 +org.eclipse.jdt.core.compiler.compliance=1.6 +org.eclipse.jdt.core.compiler.source=1.6 diff --git a/.tx/config b/.tx/config new file mode 100644 index 00000000..1cf8cf29 --- /dev/null +++ b/.tx/config @@ -0,0 +1,15 @@ +[main] +host = https://www.transifex.com + +[espeak-for-android.stringsxml] +file_filter = android/res/values-/strings.xml +source_file = android/res/values/strings.xml +source_lang = en +trans.en_GB = android/res/values-en-rGB/strings.xml +trans.es_419 = android/res/values-es-rUS/strings.xml +trans.he = android/res/values-iw/strings.xml +trans.id = android/res/values-in/strings.xml +trans.pt_BR = android/res/values-pt-rBR/strings.xml +trans.zh_CN = android/res/values-zh-rCN/strings.xml +trans.zh_TW = android/res/values-zh-rTW/strings.xml + diff --git a/COPYING.APACHE b/COPYING.APACHE new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/COPYING.APACHE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/Makefile.am b/Makefile.am index dd28da77..31926ae0 100644 --- a/Makefile.am +++ b/Makefile.am @@ -55,6 +55,7 @@ install-exec-hook: clean-local: cd src && rm -f *.o *~ && cd .. + cd android && $(GRADLE) clean distclean-local: rm -rf espeak-ng-data/phondata-manifest @@ -105,6 +106,8 @@ docs: docs/index.html \ src/speak-ng.1.html \ README.html +check: apk-check + ##### build targets: espeak_includedir = $(includedir)/espeak @@ -305,6 +308,34 @@ phsource/phonemes.stamp: \ ESPEAK_DATA_PATH=$(PWD) src/espeak-ng --compile-phonemes && \ touch $@ +##### android targets: + +jni: + cd android && ndk-build + +apk-release: + cd android && $(GRADLE) assembleRelease + +apk-debug: + cd android && $(GRADLE) assembleDebug + +apk-check: + cd android && $(GRADLE) connectedCheck + +android/res/raw/espeakdata.zip: espeak-data/phontab dictionaries + mkdir -pv android/res/raw + rm -f $@ + find espeak-data/{intonations,phondata,phonindex,phontab} | zip -@ $@ + find espeak-data/*_dict | zip -@ $@ + find espeak-data/voices -type f | grep -vF "/mb/" | zip -@ $@ + +android/res/raw/espeakdata_version: android/res/raw/espeakdata.zip + sha1sum $< | awk '{ print $$1 }' > $@ + +espeakdata: \ + android/res/raw/espeakdata.zip \ + android/res/raw/espeakdata_version + ##### dictionaries: dictionaries: \ diff --git a/android/.classpath b/android/.classpath new file mode 100644 index 00000000..3f9691c5 --- /dev/null +++ b/android/.classpath @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/android/.project b/android/.project new file mode 100644 index 00000000..f14a6c5a --- /dev/null +++ b/android/.project @@ -0,0 +1,33 @@ + + + eSpeakActivity + + + + + + com.android.ide.eclipse.adt.ResourceManagerBuilder + + + + + com.android.ide.eclipse.adt.PreCompilerBuilder + + + + + org.eclipse.jdt.core.javabuilder + + + + + com.android.ide.eclipse.adt.ApkBuilder + + + + + + com.android.ide.eclipse.adt.AndroidNature + org.eclipse.jdt.core.javanature + + diff --git a/android/.settings/org.eclipse.jdt.core.prefs b/android/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000..b080d2dd --- /dev/null +++ b/android/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,4 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 +org.eclipse.jdt.core.compiler.compliance=1.6 +org.eclipse.jdt.core.compiler.source=1.6 diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml new file mode 100644 index 00000000..3f860cbc --- /dev/null +++ b/android/AndroidManifest.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 00000000..19e09afb --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,51 @@ +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath 'com.android.tools.build:gradle:1.2.3' + } +} + +apply plugin: 'com.android.application' + +repositories { + mavenCentral() +} + +dependencies { + androidTestCompile 'org.hamcrest:hamcrest-all:1.3' +} + +android { + buildToolsVersion '19.1' + compileSdkVersion 23 + + defaultConfig { + minSdkVersion 14 + targetSdkVersion 23 + versionCode 20 + versionName "1.48.15" + } + + sourceSets { + main { + manifest.srcFile 'AndroidManifest.xml' + java.srcDirs = ['src'] + res.srcDirs = ['res'] + } + androidTest.setRoot('eSpeakTests') + androidTest { + java.srcDirs = ['eSpeakTests/src'] + } + } + + lintOptions { + abortOnError false + } +} + +tasks.withType(com.android.build.gradle.tasks.PackageApplication) { + pkgTask -> pkgTask.jniFolders = new HashSet() + pkgTask.jniFolders.add(new File(projectDir, 'libs')) +} diff --git a/android/eSpeakTests/.classpath b/android/eSpeakTests/.classpath new file mode 100644 index 00000000..8c63a6cf --- /dev/null +++ b/android/eSpeakTests/.classpath @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/android/eSpeakTests/.project b/android/eSpeakTests/.project new file mode 100644 index 00000000..a3134b73 --- /dev/null +++ b/android/eSpeakTests/.project @@ -0,0 +1,34 @@ + + + eSpeakTests + + + eSpeakActivity + + + + com.android.ide.eclipse.adt.ResourceManagerBuilder + + + + + com.android.ide.eclipse.adt.PreCompilerBuilder + + + + + org.eclipse.jdt.core.javabuilder + + + + + com.android.ide.eclipse.adt.ApkBuilder + + + + + + com.android.ide.eclipse.adt.AndroidNature + org.eclipse.jdt.core.javanature + + diff --git a/android/eSpeakTests/.settings/org.eclipse.jdt.core.prefs b/android/eSpeakTests/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000..b080d2dd --- /dev/null +++ b/android/eSpeakTests/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,4 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 +org.eclipse.jdt.core.compiler.compliance=1.6 +org.eclipse.jdt.core.compiler.source=1.6 diff --git a/android/eSpeakTests/AndroidManifest.xml b/android/eSpeakTests/AndroidManifest.xml new file mode 100644 index 00000000..76db68c6 --- /dev/null +++ b/android/eSpeakTests/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + + + + + + + \ No newline at end of file diff --git a/android/eSpeakTests/proguard-project.txt b/android/eSpeakTests/proguard-project.txt new file mode 100644 index 00000000..f2fe1559 --- /dev/null +++ b/android/eSpeakTests/proguard-project.txt @@ -0,0 +1,20 @@ +# To enable ProGuard in your project, edit project.properties +# to define the proguard.config property as described in that file. +# +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in ${sdk.dir}/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the ProGuard +# include property in project.properties. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/android/eSpeakTests/project.properties b/android/eSpeakTests/project.properties new file mode 100644 index 00000000..a3ee5ab6 --- /dev/null +++ b/android/eSpeakTests/project.properties @@ -0,0 +1,14 @@ +# This file is automatically generated by Android Tools. +# Do not modify this file -- YOUR CHANGES WILL BE ERASED! +# +# This file must be checked in Version Control Systems. +# +# To customize properties used by the Ant build system edit +# "ant.properties", and override values to adapt the script to your +# project structure. +# +# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): +#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt + +# Project target. +target=android-17 diff --git a/android/eSpeakTests/res/drawable-hdpi/ic_launcher.png b/android/eSpeakTests/res/drawable-hdpi/ic_launcher.png new file mode 100644 index 00000000..96a442e5 Binary files /dev/null and b/android/eSpeakTests/res/drawable-hdpi/ic_launcher.png differ diff --git a/android/eSpeakTests/res/drawable-ldpi/ic_launcher.png b/android/eSpeakTests/res/drawable-ldpi/ic_launcher.png new file mode 100644 index 00000000..99238729 Binary files /dev/null and b/android/eSpeakTests/res/drawable-ldpi/ic_launcher.png differ diff --git a/android/eSpeakTests/res/drawable-mdpi/ic_launcher.png b/android/eSpeakTests/res/drawable-mdpi/ic_launcher.png new file mode 100644 index 00000000..359047df Binary files /dev/null and b/android/eSpeakTests/res/drawable-mdpi/ic_launcher.png differ diff --git a/android/eSpeakTests/res/drawable-xhdpi/ic_launcher.png b/android/eSpeakTests/res/drawable-xhdpi/ic_launcher.png new file mode 100644 index 00000000..71c6d760 Binary files /dev/null and b/android/eSpeakTests/res/drawable-xhdpi/ic_launcher.png differ diff --git a/android/eSpeakTests/res/values/strings.xml b/android/eSpeakTests/res/values/strings.xml new file mode 100644 index 00000000..2f2fb5b3 --- /dev/null +++ b/android/eSpeakTests/res/values/strings.xml @@ -0,0 +1,6 @@ + + + + ESpeakTestsTest + + \ No newline at end of file diff --git a/android/eSpeakTests/src/com/reecedunn/espeak/test/CheckVoiceDataTest.java b/android/eSpeakTests/src/com/reecedunn/espeak/test/CheckVoiceDataTest.java new file mode 100644 index 00000000..1c6cf992 --- /dev/null +++ b/android/eSpeakTests/src/com/reecedunn/espeak/test/CheckVoiceDataTest.java @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2014 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.test; + +import android.app.Activity; +import android.content.Intent; +import android.speech.tts.TextToSpeech; +import android.test.ActivityUnitTestCase; + +import com.reecedunn.espeak.CheckVoiceData; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Set; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +public class CheckVoiceDataTest extends ActivityUnitTestCase +{ + Field mResultCode; + Field mResultData; + + public void setUp() throws Exception + { + super.setUp(); + + mResultCode = Activity.class.getDeclaredField("mResultCode"); + mResultCode.setAccessible(true); + + mResultData = Activity.class.getDeclaredField("mResultData"); + mResultData.setAccessible(true); + } + + public int getResultCode() throws IllegalAccessException + { + return (Integer)mResultCode.get(getActivity()); + } + + public Intent getResultData() throws IllegalAccessException + { + return (Intent)mResultData.get(getActivity()); + } + + public CheckVoiceDataTest() + { + super(CheckVoiceData.class); + } + + public Set getExpectedVoices() + { + Set expected = new HashSet(); + for (VoiceData.Voice voice : VoiceData.voices) + { + expected.add(voice.locale); + } + return expected; + } + + public void testUnavailableVoices() throws IllegalAccessException + { + Intent intent = new Intent(TextToSpeech.Engine.ACTION_CHECK_TTS_DATA); + startActivity(intent, null, null); + + assertThat(getActivity(), is(notNullValue())); + assertThat(isFinishCalled(), is(true)); + assertThat(getResultCode(), is(TextToSpeech.Engine.CHECK_VOICE_DATA_PASS)); + assertThat(getResultData(), is(notNullValue())); + + Intent data = getResultData(); + + ArrayList unavailable = data.getStringArrayListExtra(TextToSpeech.Engine.EXTRA_UNAVAILABLE_VOICES); + assertThat(unavailable, is(notNullValue())); + assertThat(unavailable.toString(), is("[]")); + } + + public void testAvailableVoicesAdded() throws IllegalAccessException + { + Intent intent = new Intent(TextToSpeech.Engine.ACTION_CHECK_TTS_DATA); + startActivity(intent, null, null); + + assertThat(getActivity(), is(notNullValue())); + assertThat(isFinishCalled(), is(true)); + assertThat(getResultCode(), is(TextToSpeech.Engine.CHECK_VOICE_DATA_PASS)); + assertThat(getResultData(), is(notNullValue())); + + Intent data = getResultData(); + + ArrayList available = data.getStringArrayListExtra(TextToSpeech.Engine.EXTRA_AVAILABLE_VOICES); + assertThat(available, is(notNullValue())); + + Set expected = getExpectedVoices(); + + Set added = new HashSet(); + for (String voice : available) + { + if (!expected.contains(voice)) + { + added.add(voice); + } + } + + assertThat(added.toString(), is("[]")); + } + + public void testAvailableVoicesRemoved() throws IllegalAccessException + { + Intent intent = new Intent(TextToSpeech.Engine.ACTION_CHECK_TTS_DATA); + startActivity(intent, null, null); + + assertThat(getActivity(), is(notNullValue())); + assertThat(isFinishCalled(), is(true)); + assertThat(getResultCode(), is(TextToSpeech.Engine.CHECK_VOICE_DATA_PASS)); + assertThat(getResultData(), is(notNullValue())); + + Intent data = getResultData(); + + ArrayList available = data.getStringArrayListExtra(TextToSpeech.Engine.EXTRA_AVAILABLE_VOICES); + assertThat(available, is(notNullValue())); + + Set expected = getExpectedVoices(); + + Set removed = new HashSet(); + for (String voice : expected) + { + if (!available.contains(voice)) + { + removed.add(voice); + } + } + + assertThat(removed.toString(), is("[]")); + } +} diff --git a/android/eSpeakTests/src/com/reecedunn/espeak/test/SpeechSynthesisTest.java b/android/eSpeakTests/src/com/reecedunn/espeak/test/SpeechSynthesisTest.java new file mode 100644 index 00000000..b7aa89ad --- /dev/null +++ b/android/eSpeakTests/src/com/reecedunn/espeak/test/SpeechSynthesisTest.java @@ -0,0 +1,346 @@ +/* + * Copyright (C) 2012-2015 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.test; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import com.reecedunn.espeak.SpeechSynthesis; +import com.reecedunn.espeak.Voice; + +import android.media.AudioFormat; +import android.speech.tts.TextToSpeech; +import android.util.Log; + +import static com.reecedunn.espeak.test.TtsMatcher.isTtsLangCode; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; +import static org.hamcrest.core.AnyOf.anyOf; + +public class SpeechSynthesisTest extends TextToSpeechTestCase +{ + public static final Locale af = new Locale("af"); // Afrikaans + public static final Locale afr = new Locale("afr"); // Afrikaans + + public static final Locale de = new Locale("de"); // German + public static final Locale de_DE = new Locale("de", "DE"); // German (Germany) + public static final Locale de_1996 = new Locale("de", "", "1996"); // German (1996 Orthography) + public static final Locale de_CH_1901 = new Locale("de", "CH", "1901"); // German (Traditional Orthography,Switzerland) + + public static final Locale deu = new Locale("deu"); // German + public static final Locale deu_DEU = new Locale("deu", "DEU"); // German (Germany) + public static final Locale deu_1996 = new Locale("deu", "", "1996"); // German (1996 Orthography) + public static final Locale deu_CHE_1901 = new Locale("deu", "CHE", "1901"); // German (Traditional Orthography,Switzerland) + + public static final Locale fr = new Locale("fr"); // French + public static final Locale fr_FR = new Locale("fr", "FR"); // French (France) + public static final Locale fr_BE = new Locale("fr", "BE"); // French (Belgium) + public static final Locale fr_1694acad = new Locale("fr", "", "1694acad"); // French (Early Modern French) + public static final Locale fr_FR_1694acad = new Locale("fr", "FR", "1694acad"); // French (Early Modern French,France) + public static final Locale fr_BE_1694acad = new Locale("fr", "BE", "1694acad"); // French (Early Modern French,Belgium) + + public static final Locale fra = new Locale("fra"); // French + public static final Locale fra_FRA = new Locale("fra", "FRA"); // French (France) + public static final Locale fra_BEL = new Locale("fra", "BEL"); // French (Belgium) + public static final Locale fra_1694acad = new Locale("fra", "", "1694acad"); // French (Early Modern French) + public static final Locale fra_FRA_1694acad = new Locale("fra", "FRA", "1694acad"); // French (Early Modern French,France) + public static final Locale fra_BEL_1694acad = new Locale("fra", "BEL", "1694acad"); // French (Early Modern French,Belgium) + + public static final Locale hy = new Locale("hy"); // Armenian + public static final Locale hy_AM = new Locale("hy", "AM"); // Armenian (Armenia) + public static final Locale hy_arevela = new Locale("hy", "", "arevela"); // Armenian (Eastern) + public static final Locale hy_arevmda = new Locale("hy", "", "arevmda"); // Armenian (Western) + public static final Locale hy_AM_arevela = new Locale("hy", "AM", "arevela"); // Armenian (Eastern,Armenia) + public static final Locale hy_AM_arevmda = new Locale("hy", "AM", "arevmda"); // Armenian (Western,Armenia) + + public static final Locale hye = new Locale("hye"); // Armenian + public static final Locale hye_ARM = new Locale("hye", "ARM"); // Armenian (Armenia) + public static final Locale hye_arevela = new Locale("hye", "", "arevela"); // Armenian (Eastern) + public static final Locale hye_arevmda = new Locale("hye", "", "arevmda"); // Armenian (Western) + public static final Locale hye_ARM_arevela = new Locale("hye", "ARM", "arevela"); // Armenian (Eastern,Armenia) + public static final Locale hye_ARM_arevmda = new Locale("hye", "ARM", "arevmda"); // Armenian (Western,Armenia) + + public static final Locale en = new Locale("en"); // English + public static final Locale en_GB = new Locale("en", "GB"); // English (Great Britain) + public static final Locale en_US = new Locale("en", "US"); // English (USA) + public static final Locale en_scotland = new Locale("en", "", "scotland"); // English (Scottish) + public static final Locale en_GB_scotland = new Locale("en", "GB", "scotland"); // English (Scottish,Great Britain) + public static final Locale en_GB_north = new Locale("en", "GB", "north"); // English (North,Great Britain) + + public static final Locale eng = new Locale("en"); // English + public static final Locale eng_GBR = new Locale("en", "GBR"); // English (Great Britain) + public static final Locale eng_USA = new Locale("en", "USA"); // English (USA) + public static final Locale eng_scotland = new Locale("en", "", "scotland"); // English (Scottish) + public static final Locale eng_GBR_scotland = new Locale("en", "GBR", "scotland"); // English (Scottish,Great Britain) + public static final Locale eng_GBR_north = new Locale("en", "GBR", "north"); // English (North,Great Britain) + + private SpeechSynthesis.SynthReadyCallback mCallback = new SpeechSynthesis.SynthReadyCallback() + { + @Override + public void onSynthDataReady(byte[] audioData) + { + } + + @Override + public void onSynthDataComplete() + { + } + }; + + private Map mVoices = null; + private Set mAdded = new HashSet(); + private Set mRemoved = new HashSet(); + + public Map getVoices() + { + if (mVoices == null) + { + final SpeechSynthesis synth = new SpeechSynthesis(getContext(), mCallback); + mVoices = new HashMap(); + for (Voice voice : synth.getAvailableVoices()) { + assertThat(mVoices.get(voice.name), is(nullValue())); + mVoices.put(voice.name, voice); + } + assertThat(mVoices, is(notNullValue())); + + Set voices = new HashSet(); + for (Voice data : mVoices.values()) + { + voices.add(data.name); + } + + Set expected = new HashSet(); + for (VoiceData.Voice data : VoiceData.voices) + { + expected.add(data.name); + } + + for (String voice : voices) + { + if (!expected.contains(voice)) + { + mAdded.add(voice); + } + } + + for (String voice : expected) + { + if (!voices.contains(voice)) + { + mRemoved.add(voice); + } + } + } + return mVoices; + } + + public Voice getVoice(String name) + { + return getVoices().get(name); + } + + public void testConstruction() + { + final SpeechSynthesis synth = new SpeechSynthesis(getContext(), mCallback); + assertThat(synth.getSampleRate(), is(22050)); + assertThat(synth.getChannelCount(), is(1)); + assertThat(synth.getAudioFormat(), is(AudioFormat.ENCODING_PCM_16BIT)); + } + + public void testJavaToIanaLanguageCode() + { + for (VoiceData.Voice data : VoiceData.voices) + { + assertThat(SpeechSynthesis.getIanaLanguageCode(data.javaLanguage), is(data.ianaLanguage)); + } + } + + public void testJavaToIanaCountryCode() + { + for (VoiceData.Voice data : VoiceData.voices) + { + assertThat(SpeechSynthesis.getIanaCountryCode(data.javaCountry), is(data.ianaCountry)); + } + } + + public void testAddedVoices() + { + getVoices(); // Ensure that the voice data has been populated. + assertThat(mAdded.toString(), is("[]")); + } + + public void testRemovedVoices() + { + getVoices(); // Ensure that the voice data has been populated. + assertThat(mRemoved.toString(), is("[]")); + } + + public void testVoiceData() + { + for (VoiceData.Voice data : VoiceData.voices) + { + if (mRemoved.contains(data.name)) + { + Log.i("SpeechSynthesisTest", "Skipping the missing voice '" + data.name + "'"); + continue; + } + + String context = "[voice]"; + try + { + final Voice voice = getVoice(data.name); + assertThat(voice, is(notNullValue())); + + context = "[name]"; + assertThat(voice.name, is(data.name)); + context = "[identifier]"; + assertThat(voice.identifier, is(data.identifier)); + context = "[age]"; + assertThat(voice.age, is(0)); + context = "[gender]"; + assertThat(voice.gender, is(data.gender)); + context = "[locale:language]"; + assertThat(voice.locale.getLanguage(), is(data.ianaLanguage)); + context = "[locale:iso3language]"; + assertThat(voice.locale.getISO3Language(), is(data.javaLanguage)); + context = "[locale:country]"; + assertThat(voice.locale.getCountry(), is(data.ianaCountry)); + context = "[locale:iso3country]"; + assertThat(voice.locale.getISO3Country(), is(data.javaCountry)); + context = "[locale:variant]"; + assertThat(voice.locale.getVariant(), is(data.variant)); + context = "[toString]"; + assertThat(voice.toString(), is(data.locale)); + } + catch (AssertionError e) + { + throw new VoiceData.Exception(data, context, e); + } + } + } + + public void testMatchVoiceWithLanguage() + { + final Voice voice = getVoice("de"); // language="de" country="" variant="" + assertThat(voice, is(notNullValue())); + + assertThat(voice.match(fr), isTtsLangCode(TextToSpeech.LANG_NOT_SUPPORTED)); + assertThat(voice.match(fr_BE), isTtsLangCode(TextToSpeech.LANG_NOT_SUPPORTED)); + assertThat(voice.match(fr_1694acad), isTtsLangCode(TextToSpeech.LANG_NOT_SUPPORTED)); + assertThat(voice.match(fr_FR_1694acad), isTtsLangCode(TextToSpeech.LANG_NOT_SUPPORTED)); + + assertThat(voice.match(de), isTtsLangCode(TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE)); + assertThat(voice.match(de_1996), isTtsLangCode(TextToSpeech.LANG_COUNTRY_AVAILABLE)); + assertThat(voice.match(de_DE), isTtsLangCode(TextToSpeech.LANG_AVAILABLE)); + assertThat(voice.match(de_CH_1901), isTtsLangCode(TextToSpeech.LANG_AVAILABLE)); + + assertThat(voice.match(deu), isTtsLangCode(TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE)); + assertThat(voice.match(deu_1996), isTtsLangCode(TextToSpeech.LANG_COUNTRY_AVAILABLE)); + assertThat(voice.match(deu_DEU), isTtsLangCode(TextToSpeech.LANG_AVAILABLE)); + assertThat(voice.match(deu_CHE_1901), isTtsLangCode(TextToSpeech.LANG_AVAILABLE)); + } + + public void testMatchVoiceWithLanguageAndCountry() + { + final Voice voice = getVoice("fr-fr"); // language="fr" country="fr" variant="" + assertThat(voice, is(notNullValue())); + + assertThat(voice.match(de), isTtsLangCode(TextToSpeech.LANG_NOT_SUPPORTED)); + assertThat(voice.match(de_1996), isTtsLangCode(TextToSpeech.LANG_NOT_SUPPORTED)); + assertThat(voice.match(de_DE), isTtsLangCode(TextToSpeech.LANG_NOT_SUPPORTED)); + assertThat(voice.match(de_CH_1901), isTtsLangCode(TextToSpeech.LANG_NOT_SUPPORTED)); + + assertThat(voice.match(fr), isTtsLangCode(TextToSpeech.LANG_AVAILABLE)); + assertThat(voice.match(fr_FR), isTtsLangCode(TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE)); + assertThat(voice.match(fr_BE), isTtsLangCode(TextToSpeech.LANG_AVAILABLE)); + assertThat(voice.match(fr_1694acad), isTtsLangCode(TextToSpeech.LANG_AVAILABLE)); + assertThat(voice.match(fr_FR_1694acad), isTtsLangCode(TextToSpeech.LANG_COUNTRY_AVAILABLE)); + assertThat(voice.match(fr_BE_1694acad), isTtsLangCode(TextToSpeech.LANG_AVAILABLE)); + + assertThat(voice.match(fra), isTtsLangCode(TextToSpeech.LANG_AVAILABLE)); + assertThat(voice.match(fra_FRA), isTtsLangCode(TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE)); + assertThat(voice.match(fra_BEL), isTtsLangCode(TextToSpeech.LANG_AVAILABLE)); + assertThat(voice.match(fra_1694acad), isTtsLangCode(TextToSpeech.LANG_AVAILABLE)); + assertThat(voice.match(fra_FRA_1694acad), isTtsLangCode(TextToSpeech.LANG_COUNTRY_AVAILABLE)); + assertThat(voice.match(fra_BEL_1694acad), isTtsLangCode(TextToSpeech.LANG_AVAILABLE)); + } + + public void testMatchVoiceWithLanguageCountryAndVariant() + { + final Voice voice = getVoice("en-sc"); // language="en" country="GB" variant="scotland" + assertThat(voice, is(notNullValue())); + + assertThat(voice.match(de), isTtsLangCode(TextToSpeech.LANG_NOT_SUPPORTED)); + assertThat(voice.match(de_1996), isTtsLangCode(TextToSpeech.LANG_NOT_SUPPORTED)); + assertThat(voice.match(de_DE), isTtsLangCode(TextToSpeech.LANG_NOT_SUPPORTED)); + assertThat(voice.match(de_CH_1901), isTtsLangCode(TextToSpeech.LANG_NOT_SUPPORTED)); + + assertThat(voice.match(en), isTtsLangCode(TextToSpeech.LANG_AVAILABLE)); + assertThat(voice.match(en_GB), isTtsLangCode(TextToSpeech.LANG_COUNTRY_AVAILABLE)); + assertThat(voice.match(en_US), isTtsLangCode(TextToSpeech.LANG_AVAILABLE)); + assertThat(voice.match(en_scotland), isTtsLangCode(TextToSpeech.LANG_AVAILABLE)); // NOTE: Android does not support LANG_VAR_AVAILABLE. + assertThat(voice.match(en_GB_scotland), isTtsLangCode(TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE)); + assertThat(voice.match(en_GB_north), isTtsLangCode(TextToSpeech.LANG_COUNTRY_AVAILABLE)); + + assertThat(voice.match(eng), isTtsLangCode(TextToSpeech.LANG_AVAILABLE)); + assertThat(voice.match(eng_GBR), isTtsLangCode(TextToSpeech.LANG_COUNTRY_AVAILABLE)); + assertThat(voice.match(eng_USA), isTtsLangCode(TextToSpeech.LANG_AVAILABLE)); + assertThat(voice.match(eng_scotland), isTtsLangCode(TextToSpeech.LANG_AVAILABLE)); // NOTE: Android does not support LANG_VAR_AVAILABLE. + assertThat(voice.match(eng_GBR_scotland), isTtsLangCode(TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE)); + assertThat(voice.match(eng_GBR_north), isTtsLangCode(TextToSpeech.LANG_COUNTRY_AVAILABLE)); + } + + public void testGetSampleText() + { + final String[] currentLocales = getContext().getResources().getAssets().getLocales(); + for (VoiceData.Voice data : VoiceData.voices) + { + if (mRemoved.contains(data.name)) + { + Log.i("SpeechSynthesisTest", "Skipping the missing voice '" + data.name + "'"); + continue; + } + + String context = null; + try + { + final Locale ianaLocale = new Locale(data.ianaLanguage, data.ianaCountry, data.variant); + context = "[iana:sample-text]"; + assertThat(SpeechSynthesis.getSampleText(getContext(), ianaLocale), isIn(data.sampleText)); + context = "[iana:resource-locale]"; + assertThat(getContext().getResources().getAssets().getLocales(), is(currentLocales)); + + if (!data.javaLanguage.equals("")) + { + final Locale javaLocale = new Locale(data.javaLanguage, data.javaCountry, data.variant); + context = "[java:sample-text]"; + assertThat(SpeechSynthesis.getSampleText(getContext(), javaLocale), isIn(data.sampleText)); + context = "[java:resource-locale]"; + assertThat(getContext().getResources().getAssets().getLocales(), is(currentLocales)); + } + } + catch (AssertionError e) + { + throw new VoiceData.Exception(data, context, e); + } + } + } +} diff --git a/android/eSpeakTests/src/com/reecedunn/espeak/test/TextToSpeechServiceTest.java b/android/eSpeakTests/src/com/reecedunn/espeak/test/TextToSpeechServiceTest.java new file mode 100644 index 00000000..0bc2fa42 --- /dev/null +++ b/android/eSpeakTests/src/com/reecedunn/espeak/test/TextToSpeechServiceTest.java @@ -0,0 +1,257 @@ +/* + * Copyright (C) 2015 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.test; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.os.Build; +import android.speech.tts.TextToSpeech; +import android.test.AndroidTestCase; + +import com.reecedunn.espeak.TtsService; +import com.reecedunn.espeak.Voice; + +import java.util.Locale; +import java.util.Set; + +import static com.reecedunn.espeak.test.TtsMatcher.isTtsLangCode; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +public class TextToSpeechServiceTest extends AndroidTestCase +{ + public class TtsServiceTest extends TtsService + { + public TtsServiceTest(Context context) + { + attachBaseContext(context); + } + + public String[] onGetLanguage() { + return super.onGetLanguage(); + } + + public int onIsLanguageAvailable(String language, String country, String variant) { + return super.onIsLanguageAvailable(language, country, variant); + } + + public int onLoadLanguage(String language, String country, String variant) { + return super.onLoadLanguage(language, country, variant); + } + + public Set onGetFeaturesForLanguage(String language, String country, String variant) { + return super.onGetFeaturesForLanguage(language, country, variant); + } + + public Voice getActiveVoice() { + return mMatchingVoice; + } + + @SuppressLint("NewApi") + private android.speech.tts.Voice getVoice(String name) { + for (android.speech.tts.Voice voice : onGetVoices()) { + if (voice.getName().equals(name)) { + return voice; + } + } + return null; + } + } + + private TtsServiceTest mService = null; + + @Override + public void setUp() throws Exception + { + mService = new TtsServiceTest(getContext()); + mService.onCreate(); + } + + @Override + public void tearDown() + { + if (mService != null) + { + mService.onDestroy(); + mService = null; + } + } + + private void checkLanguage(String[] locale, String language, String country, String variant) { + assertThat(locale.length, is(3)); + assertThat(locale[0], is(language)); + assertThat(locale[1], is(country)); + assertThat(locale[2], is(variant)); + } + + public void testOnLoadLanguage() { + assertThat(mService.onLoadLanguage("eng", "", ""), isTtsLangCode(TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE)); + checkLanguage(mService.onGetLanguage(), "eng", "", ""); + assertThat(mService.getActiveVoice(), is(notNullValue())); + assertThat(mService.getActiveVoice().name, is("en")); + + assertThat(mService.onLoadLanguage("eng", "USA", ""), isTtsLangCode(TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE)); + checkLanguage(mService.onGetLanguage(), "eng", "USA", ""); + assertThat(mService.getActiveVoice(), is(notNullValue())); + assertThat(mService.getActiveVoice().name, is("en-us")); + + assertThat(mService.onLoadLanguage("eng", "GBR", "scotland"), isTtsLangCode(TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE)); + checkLanguage(mService.onGetLanguage(), "eng", "GBR", "scotland"); + assertThat(mService.getActiveVoice(), is(notNullValue())); + assertThat(mService.getActiveVoice().name, is("en-sc")); + + assertThat(mService.onLoadLanguage("eng", "USA", "rp"), isTtsLangCode(TextToSpeech.LANG_COUNTRY_AVAILABLE)); + checkLanguage(mService.onGetLanguage(), "eng", "USA", ""); + assertThat(mService.getActiveVoice(), is(notNullValue())); + assertThat(mService.getActiveVoice().name, is("en-us")); + + assertThat(mService.onLoadLanguage("eng", "", "scotland"), isTtsLangCode(TextToSpeech.LANG_COUNTRY_AVAILABLE)); + checkLanguage(mService.onGetLanguage(), "eng", "", ""); + assertThat(mService.getActiveVoice(), is(notNullValue())); + assertThat(mService.getActiveVoice().name, is("en")); + + assertThat(mService.onLoadLanguage("eng", "FRA", "rp"), isTtsLangCode(TextToSpeech.LANG_AVAILABLE)); + checkLanguage(mService.onGetLanguage(), "eng", "", ""); + assertThat(mService.getActiveVoice(), is(notNullValue())); + assertThat(mService.getActiveVoice().name, is("en")); + + assertThat(mService.onLoadLanguage("eng", "FRA", ""), isTtsLangCode(TextToSpeech.LANG_AVAILABLE)); + checkLanguage(mService.onGetLanguage(), "eng", "", ""); + assertThat(mService.getActiveVoice(), is(notNullValue())); + assertThat(mService.getActiveVoice().name, is("en")); + + assertThat(mService.onLoadLanguage("ine", "", ""), isTtsLangCode(TextToSpeech.LANG_NOT_SUPPORTED)); + checkLanguage(mService.onGetLanguage(), "eng", "", ""); + assertThat(mService.getActiveVoice(), is(notNullValue())); + assertThat(mService.getActiveVoice().name, is("en")); + } + + public void testOnIsLanguageAvailable() { + assertThat(mService.onLoadLanguage("vie", "VNM", "saigon"), isTtsLangCode(TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE)); + + assertThat(mService.onIsLanguageAvailable("eng", "", ""), isTtsLangCode(TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE)); + checkLanguage(mService.onGetLanguage(), "vie", "VNM", "saigon"); + assertThat(mService.getActiveVoice(), is(notNullValue())); + assertThat(mService.getActiveVoice().name, is("vi-sgn")); + + assertThat(mService.onIsLanguageAvailable("eng", "USA", ""), isTtsLangCode(TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE)); + checkLanguage(mService.onGetLanguage(), "vie", "VNM", "saigon"); + assertThat(mService.getActiveVoice(), is(notNullValue())); + assertThat(mService.getActiveVoice().name, is("vi-sgn")); + + assertThat(mService.onIsLanguageAvailable("eng", "GBR", "scotland"), isTtsLangCode(TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE)); + checkLanguage(mService.onGetLanguage(), "vie", "VNM", "saigon"); + assertThat(mService.getActiveVoice(), is(notNullValue())); + assertThat(mService.getActiveVoice().name, is("vi-sgn")); + + assertThat(mService.onIsLanguageAvailable("eng", "USA", "rp"), isTtsLangCode(TextToSpeech.LANG_COUNTRY_AVAILABLE)); + checkLanguage(mService.onGetLanguage(), "vie", "VNM", "saigon"); + assertThat(mService.getActiveVoice(), is(notNullValue())); + assertThat(mService.getActiveVoice().name, is("vi-sgn")); + + assertThat(mService.onIsLanguageAvailable("eng", "", "scotland"), isTtsLangCode(TextToSpeech.LANG_COUNTRY_AVAILABLE)); + checkLanguage(mService.onGetLanguage(), "vie", "VNM", "saigon"); + assertThat(mService.getActiveVoice(), is(notNullValue())); + assertThat(mService.getActiveVoice().name, is("vi-sgn")); + + assertThat(mService.onIsLanguageAvailable("eng", "FRA", "rp"), isTtsLangCode(TextToSpeech.LANG_AVAILABLE)); + checkLanguage(mService.onGetLanguage(), "vie", "VNM", "saigon"); + assertThat(mService.getActiveVoice(), is(notNullValue())); + assertThat(mService.getActiveVoice().name, is("vi-sgn")); + + assertThat(mService.onIsLanguageAvailable("eng", "FRA", ""), isTtsLangCode(TextToSpeech.LANG_AVAILABLE)); + checkLanguage(mService.onGetLanguage(), "vie", "VNM", "saigon"); + assertThat(mService.getActiveVoice(), is(notNullValue())); + assertThat(mService.getActiveVoice().name, is("vi-sgn")); + + assertThat(mService.onIsLanguageAvailable("ine", "", ""), isTtsLangCode(TextToSpeech.LANG_NOT_SUPPORTED)); + checkLanguage(mService.onGetLanguage(), "vie", "VNM", "saigon"); + assertThat(mService.getActiveVoice(), is(notNullValue())); + assertThat(mService.getActiveVoice().name, is("vi-sgn")); + } + + public void testOnGetDefaultVoiceNameFor() { + assertThat(mService.onLoadLanguage("vie", "VNM", "saigon"), isTtsLangCode(TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE)); + + assertThat(mService.onGetDefaultVoiceNameFor("eng", "", ""), is("en")); + checkLanguage(mService.onGetLanguage(), "vie", "VNM", "saigon"); + assertThat(mService.getActiveVoice(), is(notNullValue())); + assertThat(mService.getActiveVoice().name, is("vi-sgn")); + + assertThat(mService.onGetDefaultVoiceNameFor("eng", "USA", ""), is("en-us")); + checkLanguage(mService.onGetLanguage(), "vie", "VNM", "saigon"); + assertThat(mService.getActiveVoice(), is(notNullValue())); + assertThat(mService.getActiveVoice().name, is("vi-sgn")); + + assertThat(mService.onGetDefaultVoiceNameFor("eng", "GBR", "scotland"), is("en-sc")); + checkLanguage(mService.onGetLanguage(), "vie", "VNM", "saigon"); + assertThat(mService.getActiveVoice(), is(notNullValue())); + assertThat(mService.getActiveVoice().name, is("vi-sgn")); + + assertThat(mService.onGetDefaultVoiceNameFor("eng", "USA", "rp"), is("en-us")); + checkLanguage(mService.onGetLanguage(), "vie", "VNM", "saigon"); + assertThat(mService.getActiveVoice(), is(notNullValue())); + assertThat(mService.getActiveVoice().name, is("vi-sgn")); + + assertThat(mService.onGetDefaultVoiceNameFor("eng", "", "scotland"), is("en")); + checkLanguage(mService.onGetLanguage(), "vie", "VNM", "saigon"); + assertThat(mService.getActiveVoice(), is(notNullValue())); + assertThat(mService.getActiveVoice().name, is("vi-sgn")); + + assertThat(mService.onGetDefaultVoiceNameFor("eng", "FRA", "rp"), is("en")); + checkLanguage(mService.onGetLanguage(), "vie", "VNM", "saigon"); + assertThat(mService.getActiveVoice(), is(notNullValue())); + assertThat(mService.getActiveVoice().name, is("vi-sgn")); + + assertThat(mService.onGetDefaultVoiceNameFor("eng", "FRA", ""), is("en")); + checkLanguage(mService.onGetLanguage(), "vie", "VNM", "saigon"); + assertThat(mService.getActiveVoice(), is(notNullValue())); + assertThat(mService.getActiveVoice().name, is("vi-sgn")); + + assertThat(mService.onGetDefaultVoiceNameFor("ine", "", ""), is(nullValue())); + checkLanguage(mService.onGetLanguage(), "vie", "VNM", "saigon"); + assertThat(mService.getActiveVoice(), is(notNullValue())); + assertThat(mService.getActiveVoice().name, is("vi-sgn")); + } + + public void testLanguages() { + for (VoiceData.Voice data : VoiceData.voices) + { + assertThat(mService.onIsLanguageAvailable(data.javaLanguage, data.javaCountry, data.variant), isTtsLangCode(TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE)); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + assertThat(mService.onLoadLanguage(data.javaLanguage, data.javaCountry, data.variant), isTtsLangCode(TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE)); + checkLanguage(mService.onGetLanguage(), data.javaLanguage, data.javaCountry, data.variant); + } else { + assertThat(mService.onGetDefaultVoiceNameFor(data.javaLanguage, data.javaCountry, data.variant), is(data.name)); + assertThat(mService.onLoadVoice(data.name), is(TextToSpeech.SUCCESS)); + + android.speech.tts.Voice voice = mService.getVoice(data.name); + assertThat(voice, is(notNullValue())); + + Locale locale = voice.getLocale(); + assertThat(locale, is(notNullValue())); + assertThat(locale.getISO3Language(), is(data.javaLanguage)); + assertThat(locale.getISO3Country(), is(data.javaCountry)); + assertThat(locale.getVariant(), is(data.variant)); + + Set features = mService.onGetFeaturesForLanguage(data.javaLanguage, data.javaCountry, data.variant); + assertThat(features, is(notNullValue())); + assertThat(features.size(), is(0)); + } + } + } +} diff --git a/android/eSpeakTests/src/com/reecedunn/espeak/test/TextToSpeechTest.java b/android/eSpeakTests/src/com/reecedunn/espeak/test/TextToSpeechTest.java new file mode 100644 index 00000000..23fb8835 --- /dev/null +++ b/android/eSpeakTests/src/com/reecedunn/espeak/test/TextToSpeechTest.java @@ -0,0 +1,242 @@ +/* + * Copyright (C) 2012-2015 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.test; + +import java.util.HashSet; +import java.util.Locale; +import java.util.Set; + +import android.annotation.SuppressLint; +import android.os.Build; +import android.speech.tts.TextToSpeech; + +import static com.reecedunn.espeak.test.TtsMatcher.isTtsLangCode; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +public class TextToSpeechTest extends TextToSpeechTestCase +{ + private Set mVoices = null; + private Set mAdded = new HashSet(); + private Set mRemoved = new HashSet(); + + @SuppressLint("NewApi") + public Set getVoices() + { + if (mVoices == null) + { + Set voiceData = getEngine().getVoices(); + assertThat(voiceData, is(notNullValue())); + + mVoices = new HashSet(); + for (android.speech.tts.Voice voice : voiceData) + { + mVoices.add(voice); + } + + Set voices = new HashSet(); + for (Object data : mVoices) + { + voices.add(((android.speech.tts.Voice)data).getName()); + } + + Set expected = new HashSet(); + for (VoiceData.Voice data : VoiceData.voices) + { + expected.add(data.name); + } + + for (String voice : voices) + { + if (!expected.contains(voice)) + { + mAdded.add(voice); + } + } + + for (String voice : expected) + { + if (!voices.contains(voice)) + { + mRemoved.add(voice); + } + } + } + return mVoices; + } + + public VoiceData.Voice getVoiceData(String name) + { + for (VoiceData.Voice voice : VoiceData.voices) + { + if (voice.name.equals(name)) + { + return voice; + } + } + return null; + } + + public void testAddedVoices() + { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) + return; + + getVoices(); // Ensure that the voice data has been populated. + assertThat(mAdded.toString(), is("[]")); + } + + public void testRemovedVoices() + { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) + return; + + getVoices(); // Ensure that the voice data has been populated. + assertThat(mRemoved.toString(), is("[]")); + } + + @SuppressLint("NewApi") + public void testVoices() + { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) + return; + + getVoices(); // Ensure that the voice data has been populated. + for (Object item : mVoices) + { + android.speech.tts.Voice voice = (android.speech.tts.Voice)item; + VoiceData.Voice data = getVoiceData(voice.getName()); + + assertThat(data, is(notNullValue())); + assertThat(voice.getName(), is(data.name)); + assertThat(voice.getLocale().getLanguage(), is(data.ianaLanguage)); + assertThat(voice.getLocale().getCountry(), is(data.ianaCountry)); + assertThat(voice.getLocale().getVariant(), is(data.variant)); + assertThat(voice.getFeatures(), is(notNullValue())); + assertThat(voice.getFeatures().size(), is(0)); + assertThat(voice.getLatency(), is(android.speech.tts.Voice.LATENCY_VERY_LOW)); + assertThat(voice.getQuality(), is(android.speech.tts.Voice.QUALITY_NORMAL)); + + getEngine().setVoice(voice); + assertThat(getLanguage(getEngine()).getLanguage(), is(data.javaLanguage)); + assertThat(getLanguage(getEngine()).getCountry(), is(data.javaCountry)); + assertThat(getLanguage(getEngine()).getVariant(), is(data.variant)); + + android.speech.tts.Voice voice2 = getEngine().getVoice(); + assertThat(voice2, is(notNullValue())); + assertThat(voice2.getName(), is(data.name)); + assertThat(voice2.getLocale().getLanguage(), is(data.ianaLanguage)); + assertThat(voice2.getLocale().getCountry(), is(data.ianaCountry)); + assertThat(voice2.getLocale().getVariant(), is(data.variant)); + assertThat(voice2.getFeatures(), is(notNullValue())); + assertThat(voice2.getFeatures().size(), is(0)); + assertThat(voice2.getLatency(), is(android.speech.tts.Voice.LATENCY_VERY_LOW)); + assertThat(voice2.getQuality(), is(android.speech.tts.Voice.QUALITY_NORMAL)); + } + } + + public void testUnsupportedLanguage() + { + assertThat(getEngine(), is(notNullValue())); + + Locale initialLocale = getLanguage(getEngine()); + assertThat(getEngine().isLanguageAvailable(new Locale("cel")), isTtsLangCode(TextToSpeech.LANG_NOT_SUPPORTED)); + Locale language = getLanguage(getEngine()); + + assertThat(getLanguage(getEngine()).getLanguage(), is(initialLocale.getLanguage())); + assertThat(getLanguage(getEngine()).getCountry(), is(initialLocale.getCountry())); + assertThat(getLanguage(getEngine()).getVariant(), is(initialLocale.getVariant())); + } + + public void checkLanguage(VoiceData.Voice data, Locale locale, int status, String language, String country, String variant) + { + String langTag = locale.toString().replace('_', '-'); + String context = ""; + try + { + context = "isLanguageAvailable"; + assertThat(getEngine().isLanguageAvailable(locale), isTtsLangCode(status)); + context = "setLanguage"; + assertThat(getEngine().setLanguage(locale), isTtsLangCode(status)); + context = "getLanguage"; + assertThat(getLanguage(getEngine()).getLanguage(), is(language)); + assertThat(getLanguage(getEngine()).getCountry(), is(country)); + assertThat(getLanguage(getEngine()).getVariant(), is(variant)); + } + catch (AssertionError e) + { + throw new VoiceData.Exception(data, context + "|" + langTag, e); + } + } + + public void testLanguages() + { + assertThat(getEngine(), is(notNullValue())); + + for (VoiceData.Voice data : VoiceData.voices) + { + final Locale iana1 = new Locale(data.ianaLanguage, data.ianaCountry, data.variant); + final Locale iana2 = new Locale(data.ianaLanguage, data.ianaCountry, "test"); + final Locale iana3 = new Locale(data.ianaLanguage, "VU", data.variant); + + final Locale java1 = new Locale(data.javaLanguage, data.javaCountry, data.variant); + final Locale java2 = new Locale(data.javaLanguage, data.javaCountry, "test"); + final Locale java3 = new Locale(data.javaLanguage, "VUT", data.variant); + + checkLanguage(data, iana1, TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE, data.javaLanguage, data.javaCountry, data.variant); + checkLanguage(data, java1, TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE, data.javaLanguage, data.javaCountry, data.variant); + + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.LOLLIPOP) { + // Android Lollipop sets variant to "" when TextToSpeech.LANG_COUNTRY_AVAILABLE is returned. + checkLanguage(data, iana2, TextToSpeech.LANG_COUNTRY_AVAILABLE, data.javaLanguage, data.javaCountry, ""); + checkLanguage(data, java2, TextToSpeech.LANG_COUNTRY_AVAILABLE, data.javaLanguage, data.javaCountry, ""); + } else { + if (data.ianaLanguage.equals("vi") && data.ianaCountry.equals("VN")) { + checkLanguage(data, iana2, TextToSpeech.LANG_COUNTRY_AVAILABLE, data.javaLanguage, data.javaCountry, "hue"); + checkLanguage(data, java2, TextToSpeech.LANG_COUNTRY_AVAILABLE, data.javaLanguage, data.javaCountry, "hue"); + } else if (data.ianaLanguage.equals("hy") && data.ianaCountry.equals("AM")) { + checkLanguage(data, iana2, TextToSpeech.LANG_COUNTRY_AVAILABLE, data.javaLanguage, data.javaCountry, "arevmda"); + checkLanguage(data, java2, TextToSpeech.LANG_COUNTRY_AVAILABLE, data.javaLanguage, data.javaCountry, "arevmda"); + } else { + checkLanguage(data, iana2, TextToSpeech.LANG_COUNTRY_AVAILABLE, data.javaLanguage, data.javaCountry, ""); + checkLanguage(data, java2, TextToSpeech.LANG_COUNTRY_AVAILABLE, data.javaLanguage, data.javaCountry, ""); + } + } + + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.LOLLIPOP) { + // Android Lollipop sets country and variant to "" when TextToSpeech.LANG_AVAILABLE is returned. + checkLanguage(data, iana3, TextToSpeech.LANG_AVAILABLE, data.javaLanguage, "", ""); + checkLanguage(data, java3, TextToSpeech.LANG_AVAILABLE, data.javaLanguage, "", ""); + } else { + switch (data.ianaLanguage) { + case "fr": + checkLanguage(data, iana3, TextToSpeech.LANG_AVAILABLE, data.javaLanguage, "FRA", ""); + checkLanguage(data, java3, TextToSpeech.LANG_AVAILABLE, data.javaLanguage, "FRA", ""); + break; + case "pt": + checkLanguage(data, iana3, TextToSpeech.LANG_AVAILABLE, data.javaLanguage, "PRT", ""); + checkLanguage(data, java3, TextToSpeech.LANG_AVAILABLE, data.javaLanguage, "PRT", ""); + break; + default: + checkLanguage(data, iana3, TextToSpeech.LANG_AVAILABLE, data.javaLanguage, "", ""); + checkLanguage(data, java3, TextToSpeech.LANG_AVAILABLE, data.javaLanguage, "", ""); + break; + } + } + } + } +} diff --git a/android/eSpeakTests/src/com/reecedunn/espeak/test/TextToSpeechTestCase.java b/android/eSpeakTests/src/com/reecedunn/espeak/test/TextToSpeechTestCase.java new file mode 100644 index 00000000..15cf7fe8 --- /dev/null +++ b/android/eSpeakTests/src/com/reecedunn/espeak/test/TextToSpeechTestCase.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2012-2015 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.test; + +import android.os.Build; +import android.speech.tts.TextToSpeech; +import android.speech.tts.TextToSpeech.OnInitListener; +import android.test.AndroidTestCase; +import android.util.Log; + +import java.util.Locale; + +import static com.reecedunn.espeak.test.TtsMatcher.isTtsLangCode; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +public class TextToSpeechTestCase extends AndroidTestCase +{ + private TextToSpeech mEngine = null; + private boolean mInitialised = false; + private int mStatus = TextToSpeech.ERROR; + + private OnInitListener mInitCallback = new OnInitListener() + { + @Override + public void onInit(int status) + { + mStatus = status; + mInitialised = true; + } + }; + + @Override + public void setUp() throws Exception + { + try + { + // Wait until the text-to-speech engine is initialised (max: 20 seconds): + + mEngine = new TextToSpeech(getContext(), mInitCallback); + for (int count = 0; !mInitialised && count < (4 * 20); ++count) + { + Thread.sleep(250); + } + + assertThat(mInitialised, is(true)); + assertThat(mStatus, is(TextToSpeech.SUCCESS)); + assertThat(mEngine.getDefaultEngine(), is("com.reecedunn.espeak")); + + // Ensure that the voice data is installed (max: 20 seconds): + + Locale en = new Locale("en"); + int available = mEngine.isLanguageAvailable(en); + for (int count = 0; available != TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE && count < (4 * 20); ++count) + { + Thread.sleep(250); + available = mEngine.isLanguageAvailable(en); + Log.d("TextToSpeechTestCase", "setUp: available = " + available); + } + + assertThat(available, isTtsLangCode(TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE)); + } + catch (Exception e) + { + tearDown(); + throw e; + } + } + + @Override + public void tearDown() + { + if (mEngine != null) + { + mEngine.shutdown(); + mEngine = null; + } + } + + public TextToSpeech getEngine() + { + return mEngine; + } + + @SuppressWarnings("deprecation") + public static Locale getLanguage(TextToSpeech engine) { + if (engine != null) { + return engine.getLanguage(); + } + return null; + } +} diff --git a/android/eSpeakTests/src/com/reecedunn/espeak/test/TtsMatcher.java b/android/eSpeakTests/src/com/reecedunn/espeak/test/TtsMatcher.java new file mode 100644 index 00000000..b9066d62 --- /dev/null +++ b/android/eSpeakTests/src/com/reecedunn/espeak/test/TtsMatcher.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2014 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.test; + +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.TypeSafeMatcher; + +import android.speech.tts.TextToSpeech; + +public class TtsMatcher +{ + private static String ttsLangCode(final Integer value) { + if (value == null) return "null"; + switch (value.intValue()) { + case TextToSpeech.LANG_AVAILABLE: return "TextToSpeech.LANG_AVAILABLE"; + case TextToSpeech.LANG_COUNTRY_AVAILABLE: return "TextToSpeech.LANG_COUNTRY_AVAILABLE"; + case TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE: return "TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE"; + case TextToSpeech.LANG_MISSING_DATA: return "TextToSpeech.LANG_MISSING_DATA"; + case TextToSpeech.LANG_NOT_SUPPORTED: return "TextToSpeech.LANG_NOT_SUPPORTED"; + } + return value.toString(); + } + + public static Matcher isTtsLangCode(final Integer value) { + return new TypeSafeMatcher() { + @Override + protected boolean matchesSafely(Integer item) { + return item.equals(value); + } + + @Override + public void describeTo(Description description) { + description.appendText("expected constant: "); + description.appendValue(ttsLangCode(value)); + } + + @Override + public void describeMismatchSafely(final Integer item, final Description description) { + description.appendText("was "); + description.appendValue(ttsLangCode(item)); + } + }; + } +} diff --git a/android/eSpeakTests/src/com/reecedunn/espeak/test/VoiceData.java b/android/eSpeakTests/src/com/reecedunn/espeak/test/VoiceData.java new file mode 100644 index 00000000..d2edfdcc --- /dev/null +++ b/android/eSpeakTests/src/com/reecedunn/espeak/test/VoiceData.java @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2012-2015 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.test; + +import com.reecedunn.espeak.SpeechSynthesis; + +import java.util.ArrayList; +import java.util.List; + +public class VoiceData +{ + public static class Voice + { + public final String name; + public final String identifier; + public final String ianaLanguage; + public final String javaLanguage; + public final String ianaCountry; + public final String javaCountry; + public final String variant; + public final int gender; + public final String displayName; + public final String locale; + public final List sampleText = new ArrayList<>(); + + public Voice(String name, + String identifier, + String ianaLanguage, + String javaLanguage, + String ianaCountry, + String javaCountry, + String variant, + int gender, + String displayName, + String locale, + String... sampleText) + { + this.name = name; + this.identifier = identifier; + this.ianaLanguage = ianaLanguage; + this.javaLanguage = javaLanguage; + this.ianaCountry = ianaCountry; + this.javaCountry = javaCountry; + this.variant = variant; + this.gender = gender; + this.displayName = displayName; + this.locale = locale; + for (String text : sampleText) + this.sampleText.add(text); + } + } + + public static class Exception extends AssertionError + { + private static final long serialVersionUID = 1L; + + public Exception(Voice voice, String context, AssertionError error) + { + super("Voice \"" + voice.name + "\" " + context + " -- " + error); + } + } + + public static final Voice[] voices = new Voice[] { + new Voice("af", "other/af", "af", "afr", "", "", "", SpeechSynthesis.GENDER_MALE, "Afrikaans", "afr", "Hierdie is 'n voorbeeld van gesproke teks in Afrikaans"), + new Voice("am", "test/am", "am", "amh", "", "", "", SpeechSynthesis.GENDER_UNSPECIFIED, "Amharic", "amh", "ይሄ በአማርኛ ላይ የተነገረ ጽሑፍ ናሙና ነው።", "ይሄ በAmharic ላይ የተነገረ ጽሑፍ ናሙና ነው።"), + new Voice("an", "europe/an", "an", "arg", "", "", "", SpeechSynthesis.GENDER_MALE, "Aragonese", "arg", "This is a sample of text spoken in Aragonese"), + new Voice("as", "test/as", "as", "asm", "", "", "", SpeechSynthesis.GENDER_UNSPECIFIED, "Assamese", "asm", "This is a sample of text spoken in Assamese", "This is a sample of text spoken in অসমীয়া"), + new Voice("az", "test/az", "az", "aze", "", "", "", SpeechSynthesis.GENDER_UNSPECIFIED, "Azerbaijani", "aze", "This is a sample of text spoken in Azerbaijani", "This is a sample of text spoken in azərbaycan"), + new Voice("bg", "europe/bg", "bg", "bul", "", "", "", SpeechSynthesis.GENDER_UNSPECIFIED, "Bulgarian", "bul", "Това е откъс от изговорен текст на български"), + new Voice("bn", "asia/bn", "bn", "ben", "", "", "", SpeechSynthesis.GENDER_MALE, "Bengali", "ben", "This is a sample of text spoken in বাংলা", "This is a sample of text spoken in Bengali"), + new Voice("bs", "europe/bs", "bs", "bos", "", "", "", SpeechSynthesis.GENDER_MALE, "Bosnian", "bos", "This is a sample of text spoken in Bosnian", "This is a sample of text spoken in bosanski"), + new Voice("ca", "europe/ca", "ca", "cat", "", "", "", SpeechSynthesis.GENDER_MALE, "Catalan", "cat", "Aquesta és una mostra de text llegit en veu alta en català"), + new Voice("cs", "europe/cs", "cs", "ces", "", "", "", SpeechSynthesis.GENDER_MALE, "Czech", "ces", "Toto je ukázkový text namluvený v jazyce čeština"), + new Voice("cy", "europe/cy", "cy", "cym", "", "", "", SpeechSynthesis.GENDER_MALE, "Welsh", "cym", "This is a sample of text spoken in Welsh", "This is a sample of text spoken in Cymraeg"), + new Voice("da", "europe/da", "da", "dan", "", "", "", SpeechSynthesis.GENDER_MALE, "Danish", "dan", "Dette er et eksempel på talt tekst på dansk"), + new Voice("de", "de", "de", "deu", "", "", "", SpeechSynthesis.GENDER_MALE, "German", "deu", "Dies ist ein Beispieltext auf Deutsch"), + new Voice("el", "europe/el", "el", "ell", "", "", "", SpeechSynthesis.GENDER_MALE, "Greek (Modern)", "ell", "Αυτό είναι ένα δείγμα κειμένου που εκφέρεται στα Ελληνικά"), + new Voice("en", "default", "en", "eng", "", "", "", SpeechSynthesis.GENDER_MALE, "English", "eng", "This is a sample of text spoken in English"), + new Voice("en-sc", "other/en-sc", "en", "eng", "GB", "GBR", "scotland", SpeechSynthesis.GENDER_MALE, "English (Scotland)", "eng-GBR-scotland", "This is a sample of text spoken in English (United Kingdom,Scottish Standard English)"), + new Voice("en-gb", "en", "en", "eng", "GB", "GBR", "", SpeechSynthesis.GENDER_MALE, "English (UK)", "eng-GBR", "This is a sample of text spoken in English (United Kingdom)"), + new Voice("en-uk-north", "other/en-n", "en", "eng", "GB", "GBR", "north", SpeechSynthesis.GENDER_MALE, "English (Lancashire)", "eng-GBR-north", "This is a sample of text spoken in English (United Kingdom,NORTH)"), + new Voice("en-uk-rp", "other/en-rp", "en", "eng", "GB", "GBR", "rp", SpeechSynthesis.GENDER_MALE, "English (Received Pronunciation)", "eng-GBR-rp", "This is a sample of text spoken in English (United Kingdom,RP)", "This is a sample of text spoken in English (United Kingdom,rp)"), + new Voice("en-uk-wmids", "other/en-wm", "en", "eng", "GB", "GBR", "wmids", SpeechSynthesis.GENDER_MALE, "English (West Midlands)", "eng-GBR-wmids", "This is a sample of text spoken in English (United Kingdom,WMIDS)"), + new Voice("en-us", "en-us", "en", "eng", "US", "USA", "", SpeechSynthesis.GENDER_MALE, "English (US)", "eng-USA", "This is a sample of text spoken in English (United States)"), + new Voice("en-wi", "other/en-wi", "en", "eng", "JM", "JAM", "", SpeechSynthesis.GENDER_MALE, "English (Caribbean)", "eng-JAM", "This is a sample of text spoken in English (Jamaica)"), + new Voice("eo", "other/eo", "eo", "epo", "", "", "", SpeechSynthesis.GENDER_MALE, "Esperanto", "epo", "This is a sample of text spoken in Esperanto", "This is a sample of text spoken in esperanto"), + new Voice("es", "europe/es", "es", "spa", "", "", "", SpeechSynthesis.GENDER_MALE, "Spanish", "spa", "Esto es un ejemplo de texto hablado en español."), + new Voice("es-la", "es-la", "es", "spa", "MX", "MEX", "", SpeechSynthesis.GENDER_MALE, "Spanish (Latin America)", "spa-MEX", "Esto es un ejemplo de texto hablado en español (México)."), + new Voice("et", "europe/et", "et", "est", "", "", "", SpeechSynthesis.GENDER_UNSPECIFIED, "Estonian", "est", "This is a sample of text spoken in eesti", "This is a sample of text spoken in Estonian"), + new Voice("eu", "europe/eu", "eu", "eus", "", "", "", SpeechSynthesis.GENDER_UNSPECIFIED, "Basque", "eus", "This is a sample of text spoken in Basque", "This is a sample of text spoken in euskara"), + new Voice("fa", "asia/fa", "fa", "fas", "", "", "", SpeechSynthesis.GENDER_UNSPECIFIED, "Farsi (Persian)", "fas", "این یک نمونه از نوشتار گفته شده در فارسی است"), + new Voice("fi", "europe/fi", "fi", "fin", "", "", "", SpeechSynthesis.GENDER_MALE, "Finnish", "fin", "Tämä on näyte puhutusta tekstistä kielellä suomi"), + new Voice("fr-be", "europe/fr-be", "fr", "fra", "BE", "BEL", "", SpeechSynthesis.GENDER_MALE, "French (Belgium)", "fra-BEL", "Voici un exemple de texte énoncé en français (Belgique)."), + new Voice("fr-fr", "fr", "fr", "fra", "FR", "FRA", "", SpeechSynthesis.GENDER_MALE, "French (France)", "fra-FRA", "Voici un exemple de texte énoncé en français (France)."), + new Voice("ga", "europe/ga", "ga", "gle", "", "", "", SpeechSynthesis.GENDER_UNSPECIFIED, "Irish", "gle", "This is a sample of text spoken in Irish", "This is a sample of text spoken in Gaeilge"), + new Voice("gd", "test/gd", "gd", "gla", "", "", "", SpeechSynthesis.GENDER_UNSPECIFIED, "Scottish Gaelic", "gla", "This is a sample of text spoken in Scottish Gaelic", "This is a sample of text spoken in Gàidhlig"), + new Voice("grc", "other/grc", "grc", "grc", "", "", "", SpeechSynthesis.GENDER_MALE, "Greek (Ancient)", "grc", "This is a sample of text spoken in Ancient Greek"), + new Voice("gu", "asia/gu", "gu", "guj", "", "", "", SpeechSynthesis.GENDER_UNSPECIFIED, "Gujarati", "guj", "This is a sample of text spoken in Gujarati", "This is a sample of text spoken in ગુજરાતી"), + new Voice("hi", "asia/hi", "hi", "hin", "", "", "", SpeechSynthesis.GENDER_MALE, "Hindi", "hin", "यह हिन्दी में बोले गए पाठ का नमूना है"), + new Voice("hr", "europe/hr", "hr", "hrv", "", "", "", SpeechSynthesis.GENDER_MALE, "Croatian", "hrv", "Ovo je primjer teksta izgovorenog na hrvatski"), + new Voice("hu", "europe/hu", "hu", "hun", "", "", "", SpeechSynthesis.GENDER_MALE, "Hungarian", "hun", "Ez egy magyar nyelven felolvasott szöveg mintája."), + new Voice("hy", "asia/hy", "hy", "hye", "", "", "", SpeechSynthesis.GENDER_MALE, "Armenian", "hye", "This is a sample of text spoken in Armenian", "This is a sample of text spoken in հայերեն"), + new Voice("hy-west", "asia/hy-west", "hy", "hye", "AM", "ARM", "arevmda", SpeechSynthesis.GENDER_MALE, "Armenian (Western)", "hye-ARM-arevmda", "This is a sample of text spoken in Armenian (Armenia,Western Armenian)", "This is a sample of text spoken in հայերեն (Հայաստան,AREVMDA)", "This is a sample of text spoken in հայերեն (Հայաստան,արեւմտահայերէն)"), + new Voice("ia", "other/ia", "ia", "ina", "", "", "", SpeechSynthesis.GENDER_UNSPECIFIED, "Interlingua", "ina", "This is a sample of text spoken in Interlingua"), + new Voice("id", "asia/id", "in", "ind", "", "", "", SpeechSynthesis.GENDER_MALE, "Indonesia", "ind", "Ini adalah sebuah contoh teks yang diucapkan di Bahasa Indonesia"), // NOTE: 'id' is the correct ISO 639-1 code, but Android/Java uses 'in'. + new Voice("is", "europe/is", "is", "isl", "", "", "", SpeechSynthesis.GENDER_MALE, "Icelandic", "isl", "This is a sample of text spoken in íslenska", "This is a sample of text spoken in Icelandic"), + new Voice("it", "europe/it", "it", "ita", "", "", "", SpeechSynthesis.GENDER_MALE, "Italian", "ita", "Questo è un esempio di testo parlato in italiano"), + new Voice("jbo", "other/jbo", "jbo", "jbo", "", "", "", SpeechSynthesis.GENDER_UNSPECIFIED, "Lojban", "jbo", "This is a sample of text spoken in Lojban"), + new Voice("ka", "asia/ka", "ka", "kat", "", "", "", SpeechSynthesis.GENDER_UNSPECIFIED, "Georgian", "kat", "This is a sample of text spoken in Georgian", "This is a sample of text spoken in ქართული"), + new Voice("kl", "test/kl", "kl", "kal", "", "", "", SpeechSynthesis.GENDER_UNSPECIFIED, "Greenlandic", "kal", "This is a sample of text spoken in Kalaallisut", "This is a sample of text spoken in kalaallisut"), + new Voice("kn", "asia/kn", "kn", "kan", "", "", "", SpeechSynthesis.GENDER_UNSPECIFIED, "Kannada", "kan", "This is a sample of text spoken in Kannada", "This is a sample of text spoken in ಕನ್ನಡ"), + new Voice("ko", "asia/ko", "ko", "kor", "", "", "", SpeechSynthesis.GENDER_MALE, "Korean", "kor", "한국어로 읽은 텍스트 샘플입니다."), + new Voice("ku", "asia/ku", "ku", "kur", "", "", "", SpeechSynthesis.GENDER_MALE, "Kurdish", "kur", "This is a sample of text spoken in Kurdish"), + new Voice("la", "other/la", "la", "lat", "", "", "", SpeechSynthesis.GENDER_MALE, "Latin", "lat", "This is a sample of text spoken in Latin"), + new Voice("lt", "europe/lt", "lt", "lit", "", "", "", SpeechSynthesis.GENDER_MALE, "Lithuanian", "lit", "Tai teksto, sakomo lietuvių, pavyzdys"), + new Voice("lv", "europe/lv", "lv", "lav", "", "", "", SpeechSynthesis.GENDER_MALE, "Latvian", "lav", "Šis ir izrunāta teksta paraugs šādā valodā: latviešu."), + new Voice("mk", "europe/mk", "mk", "mkd", "", "", "", SpeechSynthesis.GENDER_MALE, "Macedonian", "mkd", "This is a sample of text spoken in Macedonian", "This is a sample of text spoken in македонски"), + new Voice("ml", "asia/ml", "ml", "mal", "", "", "", SpeechSynthesis.GENDER_MALE, "Malayalam", "mal", "This is a sample of text spoken in Malayalam", "This is a sample of text spoken in മലയാളം"), + new Voice("mr", "test/mr", "mr", "mar", "", "", "", SpeechSynthesis.GENDER_UNSPECIFIED, "Marathi", "mar", "This is a sample of text spoken in Marathi", "This is a sample of text spoken in मराठी"), + new Voice("ms", "asia/ms", "ms", "msa", "", "", "", SpeechSynthesis.GENDER_MALE, "Malay", "msa", "Ini adalah sampel teks yang dilafazkan dalam Bahasa Melayu", "Ini adalah sampel teks yang dilafazkan dalam Malay"), + new Voice("ne", "asia/ne", "ne", "nep", "", "", "", SpeechSynthesis.GENDER_MALE, "Nepali", "nep", "This is a sample of text spoken in Nepali", "This is a sample of text spoken in नेपाली"), + new Voice("nl", "europe/nl", "nl", "nld", "", "", "", SpeechSynthesis.GENDER_MALE, "Dutch", "nld", "Dit is een voorbeeld van tekst die is uitgesproken in het Nederlands"), + new Voice("no", "europe/no", "nb", "nob", "", "", "", SpeechSynthesis.GENDER_MALE, "Norwegian (Bokmål)", "nob", "Dette er et teksteksempel lest opp på norsk bokmål"), + new Voice("om", "test/om", "om", "orm", "", "", "", SpeechSynthesis.GENDER_UNSPECIFIED, "Oromoo", "orm", "This is a sample of text spoken in Oromo", "This is a sample of text spoken in Oromoo"), + new Voice("or", "test/or", "or", "ori", "", "", "", SpeechSynthesis.GENDER_UNSPECIFIED, "Oriya", "ori", "This is a sample of text spoken in Oriya", "This is a sample of text spoken in ଓଡ଼ିଆ"), + new Voice("pa", "asia/pa", "pa", "pan", "", "", "", SpeechSynthesis.GENDER_UNSPECIFIED, "Panjabi", "pan", "This is a sample of text spoken in Punjabi", "This is a sample of text spoken in ਪੰਜਾਬੀ"), + new Voice("pap", "test/pap", "pap", "pap", "", "", "", SpeechSynthesis.GENDER_UNSPECIFIED, "Papiamento", "pap", "This is a sample of text spoken in Papiamento"), + new Voice("pl", "europe/pl", "pl", "pol", "", "", "", SpeechSynthesis.GENDER_MALE, "Polish", "pol", "To jest przykład tekstu mówionego przy ustawieniu polski"), + new Voice("pt-br", "pt", "pt", "por", "BR", "BRA", "", SpeechSynthesis.GENDER_MALE, "Portuguese (Brazil)", "por-BRA", "Esta é uma amostra de texto falado em português (Brasil)"), + new Voice("pt-pt", "europe/pt-pt", "pt", "por", "PT", "PRT", "", SpeechSynthesis.GENDER_MALE, "Portuguese (Portugal)", "por-PRT", "Este é um exemplo de texto falado em português (Portugal)"), + new Voice("ro", "europe/ro", "ro", "ron", "", "", "", SpeechSynthesis.GENDER_MALE, "Romanian", "ron", "Aceasta este o mostră de text vorbit în română"), + new Voice("ru", "europe/ru", "ru", "rus", "", "", "", SpeechSynthesis.GENDER_MALE, "Russian", "rus", "Так синтезатор речи озвучивает русский текст"), + new Voice("si", "test/si", "si", "sin", "", "", "", SpeechSynthesis.GENDER_UNSPECIFIED, "Sinhalese", "sin", "This is a sample of text spoken in Sinhala", "This is a sample of text spoken in සිංහල"), + new Voice("sk", "europe/sk", "sk", "slk", "", "", "", SpeechSynthesis.GENDER_MALE, "Slovak", "slk", "Toto je ukážkový text nahovorený v jazyku slovenčina"), + new Voice("sl", "test/sl", "sl", "slv", "", "", "", SpeechSynthesis.GENDER_UNSPECIFIED, "Slovenian", "slv", "To je vzorec besedila, izgovorjen v slovenščina"), + new Voice("sq", "europe/sq", "sq", "sqi", "", "", "", SpeechSynthesis.GENDER_MALE, "Albanian", "sqi", "This is a sample of text spoken in Albanian", "This is a sample of text spoken in Shqip", "This is a sample of text spoken in shqip"), + new Voice("sr", "europe/sr", "sr", "srp", "", "", "", SpeechSynthesis.GENDER_MALE, "Serbian", "srp", "Ово је пример текста који је изговорен на језику Српски", "Ово је пример текста који је изговорен на језику српски"), + new Voice("sv", "europe/sv", "sv", "swe", "", "", "", SpeechSynthesis.GENDER_MALE, "Swedish", "swe", "Detta är ett textexempel som läses på svenska"), + new Voice("sw", "other/sw", "sw", "swa", "", "", "", SpeechSynthesis.GENDER_MALE, "Swahili", "swa", "Hii ni sampuli ya maandishi yaliyonenwa katika Kiswahili", "Hii ni sampuli ya maandishi yaliyonenwa katika Swahili"), + new Voice("ta", "asia/ta", "ta", "tam", "", "", "", SpeechSynthesis.GENDER_MALE, "Tamil", "tam", "This is a sample of text spoken in தமிழ்", "This is a sample of text spoken in Tamil"), + new Voice("te", "asia/te", "te", "tel", "", "", "", SpeechSynthesis.GENDER_UNSPECIFIED, "Telugu", "tel", "This is a sample of text spoken in Telugu", "This is a sample of text spoken in తెలుగు"), + new Voice("tr", "asia/tr", "tr", "tur", "", "", "", SpeechSynthesis.GENDER_MALE, "Turkish", "tur", "Bu, Türkçe dilinde seslendirilen örnek bir metindir"), + new Voice("ur", "test/ur", "ur", "urd", "", "", "", SpeechSynthesis.GENDER_UNSPECIFIED, "Urdu", "urd", "This is a sample of text spoken in Urdu", "This is a sample of text spoken in اردو"), + new Voice("vi", "asia/vi", "vi", "vie", "", "", "", SpeechSynthesis.GENDER_MALE, "Vietnamese", "vie", "Đây là mẫu văn bản được đọc bằng Tiếng Việt"), + new Voice("vi-hue", "asia/vi-hue", "vi", "vie", "VN", "VNM", "hue", SpeechSynthesis.GENDER_MALE, "Vietnamese", "vie-VNM-hue", "Đây là mẫu văn bản được đọc bằng Tiếng Việt (Việt Nam,HUE)", "Đây là mẫu văn bản được đọc bằng Tiếng Việt (Việt Nam,hue)"), + new Voice("vi-sgn", "asia/vi-sgn", "vi", "vie", "VN", "VNM", "saigon", SpeechSynthesis.GENDER_MALE, "Vietnamese", "vie-VNM-saigon", "Đây là mẫu văn bản được đọc bằng Tiếng Việt (Việt Nam,SAIGON)"), + new Voice("zh", "asia/zh", "zh", "zho", "", "", "", SpeechSynthesis.GENDER_MALE, "Chinese (Mandarin)", "zho", "This is a sample of text spoken in 中文"), + new Voice("zh-yue", "asia/zh-yue", "zh", "zho", "HK", "HKG", "", SpeechSynthesis.GENDER_MALE, "Chinese (Cantonese)", "zho-HKG", "This is a sample of text spoken in 中文 (中華人民共和國香港特別行政區)", "This is a sample of text spoken in 中文 (香港)"), + }; +} diff --git a/android/eSpeakTests/src/com/reecedunn/espeak/test/VoiceSettingsTest.java b/android/eSpeakTests/src/com/reecedunn/espeak/test/VoiceSettingsTest.java new file mode 100644 index 00000000..529290db --- /dev/null +++ b/android/eSpeakTests/src/com/reecedunn/espeak/test/VoiceSettingsTest.java @@ -0,0 +1,691 @@ +/* + * 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.test; + +import android.content.SharedPreferences; +import android.preference.PreferenceManager; + +import com.reecedunn.espeak.SpeechSynthesis; +import com.reecedunn.espeak.VoiceSettings; + +import org.json.JSONException; +import org.json.JSONObject; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +public class VoiceSettingsTest extends TextToSpeechTestCase +{ + private SpeechSynthesis.SynthReadyCallback mCallback = new SpeechSynthesis.SynthReadyCallback() + { + @Override + public void onSynthDataReady(byte[] audioData) + { + } + + @Override + public void onSynthDataComplete() + { + } + }; + + // No Settings (New Install) + + public void testNoPreferences() + { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); + SharedPreferences.Editor editor = prefs.edit(); + editor.clear(); + editor.commit(); + + SpeechSynthesis synth = new SpeechSynthesis(getContext(), mCallback); + VoiceSettings settings = new VoiceSettings(prefs, synth); + assertThat(settings.getVoiceVariant().toString(), is("male")); + assertThat(settings.getRate(), is(synth.Rate.getDefaultValue())); + assertThat(settings.getPitch(), is(synth.Pitch.getDefaultValue())); + assertThat(settings.getPitchRange(), is(synth.PitchRange.getDefaultValue())); + assertThat(settings.getVolume(), is(synth.Volume.getDefaultValue())); + assertThat(settings.getPunctuationLevel(), is(SpeechSynthesis.PUNCT_NONE)); + assertThat(settings.getPunctuationCharacters(), is(nullValue())); + + try { + JSONObject json = settings.toJSON(); + assertThat(json.opt(VoiceSettings.PRESET_VARIANT), is(instanceOf(String.class))); + assertThat((String)json.opt(VoiceSettings.PRESET_VARIANT), is("male")); + assertThat(json.opt(VoiceSettings.PRESET_RATE), is(instanceOf(Integer.class))); + assertThat((Integer)json.opt(VoiceSettings.PRESET_RATE), is(synth.Rate.getDefaultValue())); + assertThat(json.opt(VoiceSettings.PRESET_PITCH), is(instanceOf(Integer.class))); + assertThat((Integer)json.opt(VoiceSettings.PRESET_PITCH), is(synth.Pitch.getDefaultValue())); + assertThat(json.opt(VoiceSettings.PRESET_PITCH_RANGE), is(instanceOf(Integer.class))); + assertThat((Integer)json.opt(VoiceSettings.PRESET_PITCH_RANGE), is(synth.PitchRange.getDefaultValue())); + assertThat(json.opt(VoiceSettings.PRESET_VOLUME), is(instanceOf(Integer.class))); + assertThat((Integer)json.opt(VoiceSettings.PRESET_VOLUME), is(synth.Volume.getDefaultValue())); + assertThat(json.opt(VoiceSettings.PRESET_PUNCTUATION_LEVEL), is(instanceOf(String.class))); + assertThat((String)json.opt(VoiceSettings.PRESET_PUNCTUATION_LEVEL), is("none")); + assertThat(json.opt(VoiceSettings.PRESET_PUNCTUATION_CHARACTERS), is(nullValue())); + } catch (JSONException e) { + assertThat(e.toString(), is(nullValue())); // This will be false; used to report exception. + } + } + + // Old Settings + + public void testDefaultGenderMale() + { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); + SharedPreferences.Editor editor = prefs.edit(); + editor.clear(); + editor.putString("default_gender", Integer.toString(SpeechSynthesis.GENDER_MALE)); + editor.commit(); + + SpeechSynthesis synth = new SpeechSynthesis(getContext(), mCallback); + VoiceSettings settings = new VoiceSettings(prefs, synth); + assertThat(settings.getVoiceVariant().toString(), is("male")); + assertThat(settings.getRate(), is(synth.Rate.getDefaultValue())); + assertThat(settings.getPitch(), is(synth.Pitch.getDefaultValue())); + assertThat(settings.getPitchRange(), is(synth.PitchRange.getDefaultValue())); + assertThat(settings.getVolume(), is(synth.Volume.getDefaultValue())); + assertThat(settings.getPunctuationLevel(), is(SpeechSynthesis.PUNCT_NONE)); + assertThat(settings.getPunctuationCharacters(), is(nullValue())); + + try { + JSONObject json = settings.toJSON(); + assertThat(json.opt(VoiceSettings.PRESET_VARIANT), is(instanceOf(String.class))); + assertThat((String)json.opt(VoiceSettings.PRESET_VARIANT), is("male")); + assertThat(json.opt(VoiceSettings.PRESET_RATE), is(instanceOf(Integer.class))); + assertThat((Integer)json.opt(VoiceSettings.PRESET_RATE), is(synth.Rate.getDefaultValue())); + assertThat(json.opt(VoiceSettings.PRESET_PITCH), is(instanceOf(Integer.class))); + assertThat((Integer)json.opt(VoiceSettings.PRESET_PITCH), is(synth.Pitch.getDefaultValue())); + assertThat(json.opt(VoiceSettings.PRESET_PITCH_RANGE), is(instanceOf(Integer.class))); + assertThat((Integer)json.opt(VoiceSettings.PRESET_PITCH_RANGE), is(synth.PitchRange.getDefaultValue())); + assertThat(json.opt(VoiceSettings.PRESET_VOLUME), is(instanceOf(Integer.class))); + assertThat((Integer)json.opt(VoiceSettings.PRESET_VOLUME), is(synth.Volume.getDefaultValue())); + assertThat(json.opt(VoiceSettings.PRESET_PUNCTUATION_LEVEL), is(instanceOf(String.class))); + assertThat((String)json.opt(VoiceSettings.PRESET_PUNCTUATION_LEVEL), is("none")); + assertThat(json.opt(VoiceSettings.PRESET_PUNCTUATION_CHARACTERS), is(nullValue())); + } catch (JSONException e) { + assertThat(e.toString(), is(nullValue())); // This will be false; used to report exception. + } + } + + public void testDefaultGenderFemale() + { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); + SharedPreferences.Editor editor = prefs.edit(); + editor.clear(); + editor.putString("default_gender", Integer.toString(SpeechSynthesis.GENDER_FEMALE)); + editor.commit(); + + SpeechSynthesis synth = new SpeechSynthesis(getContext(), mCallback); + VoiceSettings settings = new VoiceSettings(prefs, synth); + assertThat(settings.getVoiceVariant().toString(), is("female")); + assertThat(settings.getRate(), is(synth.Rate.getDefaultValue())); + assertThat(settings.getPitch(), is(synth.Pitch.getDefaultValue())); + assertThat(settings.getPitchRange(), is(synth.PitchRange.getDefaultValue())); + assertThat(settings.getVolume(), is(synth.Volume.getDefaultValue())); + assertThat(settings.getPunctuationLevel(), is(SpeechSynthesis.PUNCT_NONE)); + assertThat(settings.getPunctuationCharacters(), is(nullValue())); + + try { + JSONObject json = settings.toJSON(); + assertThat(json.opt(VoiceSettings.PRESET_VARIANT), is(instanceOf(String.class))); + assertThat((String)json.opt(VoiceSettings.PRESET_VARIANT), is("female")); + assertThat(json.opt(VoiceSettings.PRESET_RATE), is(instanceOf(Integer.class))); + assertThat((Integer)json.opt(VoiceSettings.PRESET_RATE), is(synth.Rate.getDefaultValue())); + assertThat(json.opt(VoiceSettings.PRESET_PITCH), is(instanceOf(Integer.class))); + assertThat((Integer)json.opt(VoiceSettings.PRESET_PITCH), is(synth.Pitch.getDefaultValue())); + assertThat(json.opt(VoiceSettings.PRESET_PITCH_RANGE), is(instanceOf(Integer.class))); + assertThat((Integer)json.opt(VoiceSettings.PRESET_PITCH_RANGE), is(synth.PitchRange.getDefaultValue())); + assertThat(json.opt(VoiceSettings.PRESET_VOLUME), is(instanceOf(Integer.class))); + assertThat((Integer)json.opt(VoiceSettings.PRESET_VOLUME), is(synth.Volume.getDefaultValue())); + assertThat(json.opt(VoiceSettings.PRESET_PUNCTUATION_LEVEL), is(instanceOf(String.class))); + assertThat((String)json.opt(VoiceSettings.PRESET_PUNCTUATION_LEVEL), is("none")); + assertThat(json.opt(VoiceSettings.PRESET_PUNCTUATION_CHARACTERS), is(nullValue())); + } catch (JSONException e) { + assertThat(e.toString(), is(nullValue())); // This will be false; used to report exception. + } + } + + public void defaultRateTest(int prefValue, int settingValue, SpeechSynthesis synth) + { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); + SharedPreferences.Editor editor = prefs.edit(); + editor.clear(); + editor.putString("default_rate", Integer.toString(prefValue)); + editor.commit(); + + VoiceSettings settings = new VoiceSettings(prefs, synth); + assertThat(settings.getVoiceVariant().toString(), is("male")); + assertThat(settings.getRate(), is(settingValue)); + assertThat(settings.getPitch(), is(synth.Pitch.getDefaultValue())); + assertThat(settings.getPitchRange(), is(synth.PitchRange.getDefaultValue())); + assertThat(settings.getVolume(), is(synth.Volume.getDefaultValue())); + assertThat(settings.getPunctuationLevel(), is(SpeechSynthesis.PUNCT_NONE)); + assertThat(settings.getPunctuationCharacters(), is(nullValue())); + + try { + JSONObject json = settings.toJSON(); + assertThat(json.opt(VoiceSettings.PRESET_VARIANT), is(instanceOf(String.class))); + assertThat((String)json.opt(VoiceSettings.PRESET_VARIANT), is("male")); + assertThat(json.opt(VoiceSettings.PRESET_RATE), is(instanceOf(Integer.class))); + assertThat((Integer)json.opt(VoiceSettings.PRESET_RATE), is(settingValue)); + assertThat(json.opt(VoiceSettings.PRESET_PITCH), is(instanceOf(Integer.class))); + assertThat((Integer)json.opt(VoiceSettings.PRESET_PITCH), is(synth.Pitch.getDefaultValue())); + assertThat(json.opt(VoiceSettings.PRESET_PITCH_RANGE), is(instanceOf(Integer.class))); + assertThat((Integer)json.opt(VoiceSettings.PRESET_PITCH_RANGE), is(synth.PitchRange.getDefaultValue())); + assertThat(json.opt(VoiceSettings.PRESET_VOLUME), is(instanceOf(Integer.class))); + assertThat((Integer)json.opt(VoiceSettings.PRESET_VOLUME), is(synth.Volume.getDefaultValue())); + assertThat(json.opt(VoiceSettings.PRESET_PUNCTUATION_LEVEL), is(instanceOf(String.class))); + assertThat((String)json.opt(VoiceSettings.PRESET_PUNCTUATION_LEVEL), is("none")); + assertThat(json.opt(VoiceSettings.PRESET_PUNCTUATION_CHARACTERS), is(nullValue())); + } catch (JSONException e) { + assertThat(e.toString(), is(nullValue())); // This will be false; used to report exception. + } + } + + public void testDefaultRate() + { + SpeechSynthesis synth = new SpeechSynthesis(getContext(), mCallback); + defaultRateTest(300, 450, synth); // clamped to maximum value + defaultRateTest(200, 350, synth); + defaultRateTest(100, 175, synth); // default value + defaultRateTest( 50, 87, synth); + defaultRateTest( 25, 80, synth); // clamped to minimum value + } + + public void defaultPitchTest(int prefValue, int settingValue, SpeechSynthesis synth) + { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); + SharedPreferences.Editor editor = prefs.edit(); + editor.clear(); + editor.putString("default_pitch", Integer.toString(prefValue)); + editor.commit(); + + VoiceSettings settings = new VoiceSettings(prefs, synth); + assertThat(settings.getVoiceVariant().toString(), is("male")); + assertThat(settings.getRate(), is(synth.Rate.getDefaultValue())); + assertThat(settings.getPitch(), is(settingValue)); + assertThat(settings.getPitchRange(), is(synth.PitchRange.getDefaultValue())); + assertThat(settings.getVolume(), is(synth.Volume.getDefaultValue())); + assertThat(settings.getPunctuationLevel(), is(SpeechSynthesis.PUNCT_NONE)); + assertThat(settings.getPunctuationCharacters(), is(nullValue())); + + try { + JSONObject json = settings.toJSON(); + assertThat(json.opt(VoiceSettings.PRESET_VARIANT), is(instanceOf(String.class))); + assertThat((String)json.opt(VoiceSettings.PRESET_VARIANT), is("male")); + assertThat(json.opt(VoiceSettings.PRESET_RATE), is(instanceOf(Integer.class))); + assertThat((Integer)json.opt(VoiceSettings.PRESET_RATE), is(synth.Rate.getDefaultValue())); + assertThat(json.opt(VoiceSettings.PRESET_PITCH), is(instanceOf(Integer.class))); + assertThat((Integer)json.opt(VoiceSettings.PRESET_PITCH), is(settingValue)); + assertThat(json.opt(VoiceSettings.PRESET_PITCH_RANGE), is(instanceOf(Integer.class))); + assertThat((Integer)json.opt(VoiceSettings.PRESET_PITCH_RANGE), is(synth.PitchRange.getDefaultValue())); + assertThat(json.opt(VoiceSettings.PRESET_VOLUME), is(instanceOf(Integer.class))); + assertThat((Integer)json.opt(VoiceSettings.PRESET_VOLUME), is(synth.Volume.getDefaultValue())); + assertThat(json.opt(VoiceSettings.PRESET_PUNCTUATION_LEVEL), is(instanceOf(String.class))); + assertThat((String)json.opt(VoiceSettings.PRESET_PUNCTUATION_LEVEL), is("none")); + assertThat(json.opt(VoiceSettings.PRESET_PUNCTUATION_CHARACTERS), is(nullValue())); + } catch (JSONException e) { + assertThat(e.toString(), is(nullValue())); // This will be false; used to report exception. + } + } + + public void testDefaultPitch() + { + SpeechSynthesis synth = new SpeechSynthesis(getContext(), mCallback); + defaultPitchTest(250, 100, synth); // clamped to maximum value + defaultPitchTest(200, 100, synth); + defaultPitchTest(100, 50, synth); // default value + defaultPitchTest( 50, 25, synth); + defaultPitchTest( 0, 0, synth); + defaultPitchTest( -5, 0, synth); // clamped to minimum value + } + + // New Settings + + public void testEspeakVariant() + { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); + SharedPreferences.Editor editor = prefs.edit(); + editor.clear(); + editor.putString("espeak_variant", "klatt2-old"); + editor.commit(); + + SpeechSynthesis synth = new SpeechSynthesis(getContext(), mCallback); + VoiceSettings settings = new VoiceSettings(prefs, synth); + assertThat(settings.getVoiceVariant().toString(), is("klatt2-old")); + assertThat(settings.getRate(), is(synth.Rate.getDefaultValue())); + assertThat(settings.getPitch(), is(synth.Pitch.getDefaultValue())); + assertThat(settings.getPitchRange(), is(synth.PitchRange.getDefaultValue())); + assertThat(settings.getVolume(), is(synth.Volume.getDefaultValue())); + assertThat(settings.getPunctuationLevel(), is(SpeechSynthesis.PUNCT_NONE)); + assertThat(settings.getPunctuationCharacters(), is(nullValue())); + + try { + JSONObject json = settings.toJSON(); + assertThat(json.opt(VoiceSettings.PRESET_VARIANT), is(instanceOf(String.class))); + assertThat((String)json.opt(VoiceSettings.PRESET_VARIANT), is("klatt2-old")); + assertThat(json.opt(VoiceSettings.PRESET_RATE), is(instanceOf(Integer.class))); + assertThat((Integer)json.opt(VoiceSettings.PRESET_RATE), is(synth.Rate.getDefaultValue())); + assertThat(json.opt(VoiceSettings.PRESET_PITCH), is(instanceOf(Integer.class))); + assertThat((Integer)json.opt(VoiceSettings.PRESET_PITCH), is(synth.Pitch.getDefaultValue())); + assertThat(json.opt(VoiceSettings.PRESET_PITCH_RANGE), is(instanceOf(Integer.class))); + assertThat((Integer)json.opt(VoiceSettings.PRESET_PITCH_RANGE), is(synth.PitchRange.getDefaultValue())); + assertThat(json.opt(VoiceSettings.PRESET_VOLUME), is(instanceOf(Integer.class))); + assertThat((Integer)json.opt(VoiceSettings.PRESET_VOLUME), is(synth.Volume.getDefaultValue())); + assertThat(json.opt(VoiceSettings.PRESET_PUNCTUATION_LEVEL), is(instanceOf(String.class))); + assertThat((String)json.opt(VoiceSettings.PRESET_PUNCTUATION_LEVEL), is("none")); + assertThat(json.opt(VoiceSettings.PRESET_PUNCTUATION_CHARACTERS), is(nullValue())); + } catch (JSONException e) { + assertThat(e.toString(), is(nullValue())); // This will be false; used to report exception. + } + } + + public void espeakRateTest(int prefValue, int settingValue, SpeechSynthesis synth) + { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); + SharedPreferences.Editor editor = prefs.edit(); + editor.clear(); + editor.putString("espeak_rate", Integer.toString(prefValue)); + editor.commit(); + + VoiceSettings settings = new VoiceSettings(prefs, synth); + assertThat(settings.getVoiceVariant().toString(), is("male")); + assertThat(settings.getRate(), is(settingValue)); + assertThat(settings.getPitch(), is(synth.Pitch.getDefaultValue())); + assertThat(settings.getPitchRange(), is(synth.PitchRange.getDefaultValue())); + assertThat(settings.getVolume(), is(synth.Volume.getDefaultValue())); + assertThat(settings.getPunctuationLevel(), is(SpeechSynthesis.PUNCT_NONE)); + assertThat(settings.getPunctuationCharacters(), is(nullValue())); + + try { + JSONObject json = settings.toJSON(); + assertThat(json.opt(VoiceSettings.PRESET_VARIANT), is(instanceOf(String.class))); + assertThat((String)json.opt(VoiceSettings.PRESET_VARIANT), is("male")); + assertThat(json.opt(VoiceSettings.PRESET_RATE), is(instanceOf(Integer.class))); + assertThat((Integer)json.opt(VoiceSettings.PRESET_RATE), is(settingValue)); + assertThat(json.opt(VoiceSettings.PRESET_PITCH), is(instanceOf(Integer.class))); + assertThat((Integer)json.opt(VoiceSettings.PRESET_PITCH), is(synth.Pitch.getDefaultValue())); + assertThat(json.opt(VoiceSettings.PRESET_PITCH_RANGE), is(instanceOf(Integer.class))); + assertThat((Integer)json.opt(VoiceSettings.PRESET_PITCH_RANGE), is(synth.PitchRange.getDefaultValue())); + assertThat(json.opt(VoiceSettings.PRESET_VOLUME), is(instanceOf(Integer.class))); + assertThat((Integer)json.opt(VoiceSettings.PRESET_VOLUME), is(synth.Volume.getDefaultValue())); + assertThat(json.opt(VoiceSettings.PRESET_PUNCTUATION_LEVEL), is(instanceOf(String.class))); + assertThat((String)json.opt(VoiceSettings.PRESET_PUNCTUATION_LEVEL), is("none")); + assertThat(json.opt(VoiceSettings.PRESET_PUNCTUATION_CHARACTERS), is(nullValue())); + } catch (JSONException e) { + assertThat(e.toString(), is(nullValue())); // This will be false; used to report exception. + } + } + + public void testEspeakRate() + { + SpeechSynthesis synth = new SpeechSynthesis(getContext(), mCallback); + espeakRateTest(500, 450, synth); // clamped to maximum value + espeakRateTest(400, 400, synth); + espeakRateTest(200, 200, synth); + espeakRateTest(175, 175, synth); // default value + espeakRateTest(150, 150, synth); + espeakRateTest( 70, 80, synth); // clamped to minimum value + } + + public void espeakPitchTest(int prefValue, int settingValue, SpeechSynthesis synth) + { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); + SharedPreferences.Editor editor = prefs.edit(); + editor.clear(); + editor.putString("espeak_pitch", Integer.toString(prefValue)); + editor.commit(); + + VoiceSettings settings = new VoiceSettings(prefs, synth); + assertThat(settings.getVoiceVariant().toString(), is("male")); + assertThat(settings.getRate(), is(synth.Rate.getDefaultValue())); + assertThat(settings.getPitch(), is(settingValue)); + assertThat(settings.getPitchRange(), is(synth.PitchRange.getDefaultValue())); + assertThat(settings.getVolume(), is(synth.Volume.getDefaultValue())); + assertThat(settings.getPunctuationLevel(), is(SpeechSynthesis.PUNCT_NONE)); + assertThat(settings.getPunctuationCharacters(), is(nullValue())); + + try { + JSONObject json = settings.toJSON(); + assertThat(json.opt(VoiceSettings.PRESET_VARIANT), is(instanceOf(String.class))); + assertThat((String)json.opt(VoiceSettings.PRESET_VARIANT), is("male")); + assertThat(json.opt(VoiceSettings.PRESET_RATE), is(instanceOf(Integer.class))); + assertThat((Integer)json.opt(VoiceSettings.PRESET_RATE), is(synth.Rate.getDefaultValue())); + assertThat(json.opt(VoiceSettings.PRESET_PITCH), is(instanceOf(Integer.class))); + assertThat((Integer)json.opt(VoiceSettings.PRESET_PITCH), is(settingValue)); + assertThat(json.opt(VoiceSettings.PRESET_PITCH_RANGE), is(instanceOf(Integer.class))); + assertThat((Integer)json.opt(VoiceSettings.PRESET_PITCH_RANGE), is(synth.PitchRange.getDefaultValue())); + assertThat(json.opt(VoiceSettings.PRESET_VOLUME), is(instanceOf(Integer.class))); + assertThat((Integer)json.opt(VoiceSettings.PRESET_VOLUME), is(synth.Volume.getDefaultValue())); + assertThat(json.opt(VoiceSettings.PRESET_PUNCTUATION_LEVEL), is(instanceOf(String.class))); + assertThat((String)json.opt(VoiceSettings.PRESET_PUNCTUATION_LEVEL), is("none")); + assertThat(json.opt(VoiceSettings.PRESET_PUNCTUATION_CHARACTERS), is(nullValue())); + } catch (JSONException e) { + assertThat(e.toString(), is(nullValue())); // This will be false; used to report exception. + } + } + + public void testEspeakPitch() + { + SpeechSynthesis synth = new SpeechSynthesis(getContext(), mCallback); + espeakPitchTest(110, 100, synth); // clamped to maximum value + espeakPitchTest(100, 100, synth); + espeakPitchTest( 50, 50, synth); // default value + espeakPitchTest( 10, 10, synth); + espeakPitchTest( -5, 0, synth); // clamped to minimum value + } + + public void espeakPitchRangeTest(int prefValue, int settingValue, SpeechSynthesis synth) + { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); + SharedPreferences.Editor editor = prefs.edit(); + editor.clear(); + editor.putString("espeak_pitch_range", Integer.toString(prefValue)); + editor.commit(); + + VoiceSettings settings = new VoiceSettings(prefs, synth); + assertThat(settings.getVoiceVariant().toString(), is("male")); + assertThat(settings.getRate(), is(synth.Rate.getDefaultValue())); + assertThat(settings.getPitch(), is(synth.Pitch.getDefaultValue())); + assertThat(settings.getPitchRange(), is(settingValue)); + assertThat(settings.getVolume(), is(synth.Volume.getDefaultValue())); + assertThat(settings.getPunctuationLevel(), is(SpeechSynthesis.PUNCT_NONE)); + assertThat(settings.getPunctuationCharacters(), is(nullValue())); + + try { + JSONObject json = settings.toJSON(); + assertThat(json.opt(VoiceSettings.PRESET_VARIANT), is(instanceOf(String.class))); + assertThat((String)json.opt(VoiceSettings.PRESET_VARIANT), is("male")); + assertThat(json.opt(VoiceSettings.PRESET_RATE), is(instanceOf(Integer.class))); + assertThat((Integer)json.opt(VoiceSettings.PRESET_RATE), is(synth.Rate.getDefaultValue())); + assertThat(json.opt(VoiceSettings.PRESET_PITCH), is(instanceOf(Integer.class))); + assertThat((Integer)json.opt(VoiceSettings.PRESET_PITCH), is(synth.Pitch.getDefaultValue())); + assertThat(json.opt(VoiceSettings.PRESET_PITCH_RANGE), is(instanceOf(Integer.class))); + assertThat((Integer)json.opt(VoiceSettings.PRESET_PITCH_RANGE), is(settingValue)); + assertThat(json.opt(VoiceSettings.PRESET_VOLUME), is(instanceOf(Integer.class))); + assertThat((Integer)json.opt(VoiceSettings.PRESET_VOLUME), is(synth.Volume.getDefaultValue())); + assertThat(json.opt(VoiceSettings.PRESET_PUNCTUATION_LEVEL), is(instanceOf(String.class))); + assertThat((String)json.opt(VoiceSettings.PRESET_PUNCTUATION_LEVEL), is("none")); + assertThat(json.opt(VoiceSettings.PRESET_PUNCTUATION_CHARACTERS), is(nullValue())); + } catch (JSONException e) { + assertThat(e.toString(), is(nullValue())); // This will be false; used to report exception. + } + } + + public void testEspeakPitchRange() + { + SpeechSynthesis synth = new SpeechSynthesis(getContext(), mCallback); + espeakPitchRangeTest(110, 100, synth); // clamped to maximum value + espeakPitchRangeTest(100, 100, synth); + espeakPitchRangeTest( 50, 50, synth); // default value + espeakPitchRangeTest( 10, 10, synth); + espeakPitchRangeTest( -5, 0, synth); // clamped to minimum value + } + + public void espeakVolumeTest(int prefValue, int settingValue, SpeechSynthesis synth) + { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); + SharedPreferences.Editor editor = prefs.edit(); + editor.clear(); + editor.putString("espeak_volume", Integer.toString(prefValue)); + editor.commit(); + + VoiceSettings settings = new VoiceSettings(prefs, synth); + assertThat(settings.getVoiceVariant().toString(), is("male")); + assertThat(settings.getRate(), is(synth.Rate.getDefaultValue())); + assertThat(settings.getPitch(), is(synth.Pitch.getDefaultValue())); + assertThat(settings.getPitchRange(), is(synth.PitchRange.getDefaultValue())); + assertThat(settings.getVolume(), is(settingValue)); + assertThat(settings.getPunctuationLevel(), is(SpeechSynthesis.PUNCT_NONE)); + assertThat(settings.getPunctuationCharacters(), is(nullValue())); + + try { + JSONObject json = settings.toJSON(); + assertThat(json.opt(VoiceSettings.PRESET_VARIANT), is(instanceOf(String.class))); + assertThat((String)json.opt(VoiceSettings.PRESET_VARIANT), is("male")); + assertThat(json.opt(VoiceSettings.PRESET_RATE), is(instanceOf(Integer.class))); + assertThat((Integer)json.opt(VoiceSettings.PRESET_RATE), is(synth.Rate.getDefaultValue())); + assertThat(json.opt(VoiceSettings.PRESET_PITCH), is(instanceOf(Integer.class))); + assertThat((Integer)json.opt(VoiceSettings.PRESET_PITCH), is(synth.Pitch.getDefaultValue())); + assertThat(json.opt(VoiceSettings.PRESET_PITCH_RANGE), is(instanceOf(Integer.class))); + assertThat((Integer)json.opt(VoiceSettings.PRESET_PITCH_RANGE), is(synth.PitchRange.getDefaultValue())); + assertThat(json.opt(VoiceSettings.PRESET_VOLUME), is(instanceOf(Integer.class))); + assertThat((Integer)json.opt(VoiceSettings.PRESET_VOLUME), is(settingValue)); + assertThat(json.opt(VoiceSettings.PRESET_PUNCTUATION_LEVEL), is(instanceOf(String.class))); + assertThat((String)json.opt(VoiceSettings.PRESET_PUNCTUATION_LEVEL), is("none")); + assertThat(json.opt(VoiceSettings.PRESET_PUNCTUATION_CHARACTERS), is(nullValue())); + } catch (JSONException e) { + assertThat(e.toString(), is(nullValue())); // This will be false; used to report exception. + } + } + + public void testEspeakVolume() + { + SpeechSynthesis synth = new SpeechSynthesis(getContext(), mCallback); + espeakVolumeTest(210, 200, synth); // clamped to maximum value + espeakVolumeTest(150, 150, synth); + espeakVolumeTest(100, 100, synth); // default value + espeakVolumeTest( 50, 50, synth); + espeakVolumeTest( -5, 0, synth); // clamped to minimum value + } + + public void espeakPunctuationLevelTest(int prefValue, int settingValue, String jsonValue, SpeechSynthesis synth) + { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); + SharedPreferences.Editor editor = prefs.edit(); + editor.clear(); + editor.putString("espeak_punctuation_level", Integer.toString(prefValue)); + editor.commit(); + + VoiceSettings settings = new VoiceSettings(prefs, synth); + assertThat(settings.getVoiceVariant().toString(), is("male")); + assertThat(settings.getRate(), is(synth.Rate.getDefaultValue())); + assertThat(settings.getPitch(), is(synth.Pitch.getDefaultValue())); + assertThat(settings.getPitchRange(), is(synth.PitchRange.getDefaultValue())); + assertThat(settings.getVolume(), is(synth.Volume.getDefaultValue())); + assertThat(settings.getPunctuationLevel(), is(settingValue)); + assertThat(settings.getPunctuationCharacters(), is(nullValue())); + + try { + JSONObject json = settings.toJSON(); + assertThat(json.opt(VoiceSettings.PRESET_VARIANT), is(instanceOf(String.class))); + assertThat((String)json.opt(VoiceSettings.PRESET_VARIANT), is("male")); + assertThat(json.opt(VoiceSettings.PRESET_RATE), is(instanceOf(Integer.class))); + assertThat((Integer)json.opt(VoiceSettings.PRESET_RATE), is(synth.Rate.getDefaultValue())); + assertThat(json.opt(VoiceSettings.PRESET_PITCH), is(instanceOf(Integer.class))); + assertThat((Integer)json.opt(VoiceSettings.PRESET_PITCH), is(synth.Pitch.getDefaultValue())); + assertThat(json.opt(VoiceSettings.PRESET_PITCH_RANGE), is(instanceOf(Integer.class))); + assertThat((Integer)json.opt(VoiceSettings.PRESET_PITCH_RANGE), is(synth.PitchRange.getDefaultValue())); + assertThat(json.opt(VoiceSettings.PRESET_VOLUME), is(instanceOf(Integer.class))); + assertThat((Integer)json.opt(VoiceSettings.PRESET_VOLUME), is(synth.Volume.getDefaultValue())); + assertThat(json.opt(VoiceSettings.PRESET_PUNCTUATION_LEVEL), is(instanceOf(String.class))); + assertThat((String)json.opt(VoiceSettings.PRESET_PUNCTUATION_LEVEL), is(jsonValue)); + assertThat(json.opt(VoiceSettings.PRESET_PUNCTUATION_CHARACTERS), is(nullValue())); + } catch (JSONException e) { + assertThat(e.toString(), is(nullValue())); // This will be false; used to report exception. + } + } + + public void testEspeakPunctuationLevel() + { + SpeechSynthesis synth = new SpeechSynthesis(getContext(), mCallback); + espeakPunctuationLevelTest( 3, SpeechSynthesis.PUNCT_SOME, "some", synth); // clamped to maximum value + espeakPunctuationLevelTest( 2, SpeechSynthesis.PUNCT_SOME, "some", synth); + espeakPunctuationLevelTest( 1, SpeechSynthesis.PUNCT_ALL, "all", synth); + espeakPunctuationLevelTest( 0, SpeechSynthesis.PUNCT_NONE, "none", synth); + espeakPunctuationLevelTest(-1, SpeechSynthesis.PUNCT_NONE, "none", synth); // clamped to minimum value + } + + public void testEspeakPunctuationCharacters() + { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); + SharedPreferences.Editor editor = prefs.edit(); + editor.clear(); + editor.putString("espeak_punctuation_characters", ".?!"); + editor.commit(); + + SpeechSynthesis synth = new SpeechSynthesis(getContext(), mCallback); + VoiceSettings settings = new VoiceSettings(prefs, synth); + assertThat(settings.getVoiceVariant().toString(), is("male")); + assertThat(settings.getRate(), is(synth.Rate.getDefaultValue())); + assertThat(settings.getPitch(), is(synth.Pitch.getDefaultValue())); + assertThat(settings.getPitchRange(), is(synth.PitchRange.getDefaultValue())); + assertThat(settings.getVolume(), is(synth.Volume.getDefaultValue())); + assertThat(settings.getPunctuationLevel(), is(SpeechSynthesis.PUNCT_NONE)); + assertThat(settings.getPunctuationCharacters(), is(".?!")); + + try { + JSONObject json = settings.toJSON(); + assertThat(json.opt(VoiceSettings.PRESET_VARIANT), is(instanceOf(String.class))); + assertThat((String)json.opt(VoiceSettings.PRESET_VARIANT), is("male")); + assertThat(json.opt(VoiceSettings.PRESET_RATE), is(instanceOf(Integer.class))); + assertThat((Integer)json.opt(VoiceSettings.PRESET_RATE), is(synth.Rate.getDefaultValue())); + assertThat(json.opt(VoiceSettings.PRESET_PITCH), is(instanceOf(Integer.class))); + assertThat((Integer)json.opt(VoiceSettings.PRESET_PITCH), is(synth.Pitch.getDefaultValue())); + assertThat(json.opt(VoiceSettings.PRESET_PITCH_RANGE), is(instanceOf(Integer.class))); + assertThat((Integer)json.opt(VoiceSettings.PRESET_PITCH_RANGE), is(synth.PitchRange.getDefaultValue())); + assertThat(json.opt(VoiceSettings.PRESET_VOLUME), is(instanceOf(Integer.class))); + assertThat((Integer)json.opt(VoiceSettings.PRESET_VOLUME), is(synth.Volume.getDefaultValue())); + assertThat(json.opt(VoiceSettings.PRESET_PUNCTUATION_LEVEL), is(instanceOf(String.class))); + assertThat((String)json.opt(VoiceSettings.PRESET_PUNCTUATION_LEVEL), is("none")); + assertThat(json.opt(VoiceSettings.PRESET_PUNCTUATION_CHARACTERS), is(instanceOf(String.class))); + assertThat((String)json.opt(VoiceSettings.PRESET_PUNCTUATION_CHARACTERS), is(".?!")); + } catch (JSONException e) { + assertThat(e.toString(), is(nullValue())); // This will be false; used to report exception. + } + } + + // Mixed (Old and New) Settings + + public void testEspeakVariantWithDefaultGenderFemale() + { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); + SharedPreferences.Editor editor = prefs.edit(); + editor.clear(); + editor.putString("default_gender", Integer.toString(SpeechSynthesis.GENDER_FEMALE)); + editor.putString("espeak_variant", "klatt4"); + editor.commit(); + + SpeechSynthesis synth = new SpeechSynthesis(getContext(), mCallback); + VoiceSettings settings = new VoiceSettings(prefs, synth); + assertThat(settings.getVoiceVariant().toString(), is("klatt4")); + assertThat(settings.getRate(), is(synth.Rate.getDefaultValue())); + assertThat(settings.getPitch(), is(synth.Pitch.getDefaultValue())); + assertThat(settings.getPitchRange(), is(synth.PitchRange.getDefaultValue())); + assertThat(settings.getVolume(), is(synth.Volume.getDefaultValue())); + assertThat(settings.getPunctuationLevel(), is(SpeechSynthesis.PUNCT_NONE)); + assertThat(settings.getPunctuationCharacters(), is(nullValue())); + + try { + JSONObject json = settings.toJSON(); + assertThat(json.opt(VoiceSettings.PRESET_VARIANT), is(instanceOf(String.class))); + assertThat((String)json.opt(VoiceSettings.PRESET_VARIANT), is("klatt4")); + assertThat(json.opt(VoiceSettings.PRESET_RATE), is(instanceOf(Integer.class))); + assertThat((Integer)json.opt(VoiceSettings.PRESET_RATE), is(synth.Rate.getDefaultValue())); + assertThat(json.opt(VoiceSettings.PRESET_PITCH), is(instanceOf(Integer.class))); + assertThat((Integer)json.opt(VoiceSettings.PRESET_PITCH), is(synth.Pitch.getDefaultValue())); + assertThat(json.opt(VoiceSettings.PRESET_PITCH_RANGE), is(instanceOf(Integer.class))); + assertThat((Integer)json.opt(VoiceSettings.PRESET_PITCH_RANGE), is(synth.PitchRange.getDefaultValue())); + assertThat(json.opt(VoiceSettings.PRESET_VOLUME), is(instanceOf(Integer.class))); + assertThat((Integer)json.opt(VoiceSettings.PRESET_VOLUME), is(synth.Volume.getDefaultValue())); + assertThat(json.opt(VoiceSettings.PRESET_PUNCTUATION_LEVEL), is(instanceOf(String.class))); + assertThat((String)json.opt(VoiceSettings.PRESET_PUNCTUATION_LEVEL), is("none")); + assertThat(json.opt(VoiceSettings.PRESET_PUNCTUATION_CHARACTERS), is(nullValue())); + } catch (JSONException e) { + assertThat(e.toString(), is(nullValue())); // This will be false; used to report exception. + } + } + + public void testEspeakRateWithDefaultRate() + { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); + SharedPreferences.Editor editor = prefs.edit(); + editor.clear(); + editor.putString("default_rate", Integer.toString(50)); + editor.putString("espeak_rate", Integer.toString(200)); + editor.commit(); + + SpeechSynthesis synth = new SpeechSynthesis(getContext(), mCallback); + VoiceSettings settings = new VoiceSettings(prefs, synth); + assertThat(settings.getVoiceVariant().toString(), is("male")); + assertThat(settings.getRate(), is(200)); + assertThat(settings.getPitch(), is(synth.Pitch.getDefaultValue())); + assertThat(settings.getPitchRange(), is(synth.PitchRange.getDefaultValue())); + assertThat(settings.getVolume(), is(synth.Volume.getDefaultValue())); + assertThat(settings.getPunctuationLevel(), is(SpeechSynthesis.PUNCT_NONE)); + assertThat(settings.getPunctuationCharacters(), is(nullValue())); + + try { + JSONObject json = settings.toJSON(); + assertThat(json.opt(VoiceSettings.PRESET_VARIANT), is(instanceOf(String.class))); + assertThat((String)json.opt(VoiceSettings.PRESET_VARIANT), is("male")); + assertThat(json.opt(VoiceSettings.PRESET_RATE), is(instanceOf(Integer.class))); + assertThat((Integer)json.opt(VoiceSettings.PRESET_RATE), is(200)); + assertThat(json.opt(VoiceSettings.PRESET_PITCH), is(instanceOf(Integer.class))); + assertThat((Integer)json.opt(VoiceSettings.PRESET_PITCH), is(synth.Pitch.getDefaultValue())); + assertThat(json.opt(VoiceSettings.PRESET_PITCH_RANGE), is(instanceOf(Integer.class))); + assertThat((Integer)json.opt(VoiceSettings.PRESET_PITCH_RANGE), is(synth.PitchRange.getDefaultValue())); + assertThat(json.opt(VoiceSettings.PRESET_VOLUME), is(instanceOf(Integer.class))); + assertThat((Integer)json.opt(VoiceSettings.PRESET_VOLUME), is(synth.Volume.getDefaultValue())); + assertThat(json.opt(VoiceSettings.PRESET_PUNCTUATION_LEVEL), is(instanceOf(String.class))); + assertThat((String)json.opt(VoiceSettings.PRESET_PUNCTUATION_LEVEL), is("none")); + assertThat(json.opt(VoiceSettings.PRESET_PUNCTUATION_CHARACTERS), is(nullValue())); + } catch (JSONException e) { + assertThat(e.toString(), is(nullValue())); // This will be false; used to report exception. + } + } + + public void testEspeakPitchWithDefaultPitch() + { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); + SharedPreferences.Editor editor = prefs.edit(); + editor.clear(); + editor.putString("default_pitch", Integer.toString(50)); + editor.putString("espeak_pitch", Integer.toString(75)); + editor.commit(); + + SpeechSynthesis synth = new SpeechSynthesis(getContext(), mCallback); + VoiceSettings settings = new VoiceSettings(prefs, synth); + assertThat(settings.getVoiceVariant().toString(), is("male")); + assertThat(settings.getRate(), is(synth.Rate.getDefaultValue())); + assertThat(settings.getPitch(), is(75)); + assertThat(settings.getPitchRange(), is(synth.PitchRange.getDefaultValue())); + assertThat(settings.getVolume(), is(synth.Volume.getDefaultValue())); + assertThat(settings.getPunctuationLevel(), is(SpeechSynthesis.PUNCT_NONE)); + assertThat(settings.getPunctuationCharacters(), is(nullValue())); + + try { + JSONObject json = settings.toJSON(); + assertThat(json.opt(VoiceSettings.PRESET_VARIANT), is(instanceOf(String.class))); + assertThat((String)json.opt(VoiceSettings.PRESET_VARIANT), is("male")); + assertThat(json.opt(VoiceSettings.PRESET_RATE), is(instanceOf(Integer.class))); + assertThat((Integer)json.opt(VoiceSettings.PRESET_RATE), is(synth.Rate.getDefaultValue())); + assertThat(json.opt(VoiceSettings.PRESET_PITCH), is(instanceOf(Integer.class))); + assertThat((Integer)json.opt(VoiceSettings.PRESET_PITCH), is(75)); + assertThat(json.opt(VoiceSettings.PRESET_PITCH_RANGE), is(instanceOf(Integer.class))); + assertThat((Integer)json.opt(VoiceSettings.PRESET_PITCH_RANGE), is(synth.PitchRange.getDefaultValue())); + assertThat(json.opt(VoiceSettings.PRESET_VOLUME), is(instanceOf(Integer.class))); + assertThat((Integer)json.opt(VoiceSettings.PRESET_VOLUME), is(synth.Volume.getDefaultValue())); + assertThat(json.opt(VoiceSettings.PRESET_PUNCTUATION_LEVEL), is(instanceOf(String.class))); + assertThat((String)json.opt(VoiceSettings.PRESET_PUNCTUATION_LEVEL), is("none")); + assertThat(json.opt(VoiceSettings.PRESET_PUNCTUATION_CHARACTERS), is(nullValue())); + } catch (JSONException e) { + assertThat(e.toString(), is(nullValue())); // This will be false; used to report exception. + } + } +} diff --git a/android/eSpeakTests/src/com/reecedunn/espeak/test/VoiceVariantTest.java b/android/eSpeakTests/src/com/reecedunn/espeak/test/VoiceVariantTest.java new file mode 100644 index 00000000..b991f525 --- /dev/null +++ b/android/eSpeakTests/src/com/reecedunn/espeak/test/VoiceVariantTest.java @@ -0,0 +1,109 @@ +/* + * 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.test; + +import android.test.AndroidTestCase; + +import com.reecedunn.espeak.SpeechSynthesis; +import com.reecedunn.espeak.VoiceVariant; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +public class VoiceVariantTest extends AndroidTestCase +{ + public void testMaleVoiceVariant() + { + VoiceVariant variant = VoiceVariant.parseVoiceVariant("male"); + assertThat(variant.variant, is(nullValue())); + assertThat(variant.gender, is(SpeechSynthesis.GENDER_MALE)); + assertThat(variant.age, is(SpeechSynthesis.AGE_ANY)); + assertThat(variant.toString(), is("male")); + } + + public void testFemaleVoiceVariant() + { + VoiceVariant variant = VoiceVariant.parseVoiceVariant("female"); + assertThat(variant.variant, is(nullValue())); + assertThat(variant.gender, is(SpeechSynthesis.GENDER_FEMALE)); + assertThat(variant.age, is(SpeechSynthesis.AGE_ANY)); + assertThat(variant.toString(), is("female")); + } + + public void testNamedVoiceVariant() + { + VoiceVariant variant = VoiceVariant.parseVoiceVariant("klatt1"); + assertThat(variant.variant, is("klatt1")); + assertThat(variant.gender, is(SpeechSynthesis.GENDER_UNSPECIFIED)); + assertThat(variant.age, is(SpeechSynthesis.AGE_ANY)); + assertThat(variant.toString(), is("klatt1")); + } + + public void testMaleVoiceVariantYoung() + { + VoiceVariant variant = VoiceVariant.parseVoiceVariant("male-young"); + assertThat(variant.variant, is(nullValue())); + assertThat(variant.gender, is(SpeechSynthesis.GENDER_MALE)); + assertThat(variant.age, is(SpeechSynthesis.AGE_YOUNG)); + assertThat(variant.toString(), is("male-young")); + } + + public void testFemaleVoiceVariantYoung() + { + VoiceVariant variant = VoiceVariant.parseVoiceVariant("female-young"); + assertThat(variant.variant, is(nullValue())); + assertThat(variant.gender, is(SpeechSynthesis.GENDER_FEMALE)); + assertThat(variant.age, is(SpeechSynthesis.AGE_YOUNG)); + assertThat(variant.toString(), is("female-young")); + } + + public void testNamedVoiceVariantYoung() + { + VoiceVariant variant = VoiceVariant.parseVoiceVariant("klatt2-young"); + assertThat(variant.variant, is("klatt2")); + assertThat(variant.gender, is(SpeechSynthesis.GENDER_UNSPECIFIED)); + assertThat(variant.age, is(SpeechSynthesis.AGE_YOUNG)); + assertThat(variant.toString(), is("klatt2-young")); + } + + public void testMaleVoiceVariantOld() + { + VoiceVariant variant = VoiceVariant.parseVoiceVariant("male-old"); + assertThat(variant.variant, is(nullValue())); + assertThat(variant.gender, is(SpeechSynthesis.GENDER_MALE)); + assertThat(variant.age, is(SpeechSynthesis.AGE_OLD)); + assertThat(variant.toString(), is("male-old")); + } + + public void testFemaleVoiceVariantOld() + { + VoiceVariant variant = VoiceVariant.parseVoiceVariant("female-old"); + assertThat(variant.variant, is(nullValue())); + assertThat(variant.gender, is(SpeechSynthesis.GENDER_FEMALE)); + assertThat(variant.age, is(SpeechSynthesis.AGE_OLD)); + assertThat(variant.toString(), is("female-old")); + } + + public void testNamedVoiceVariantOld() + { + VoiceVariant variant = VoiceVariant.parseVoiceVariant("klatt3-old"); + assertThat(variant.variant, is("klatt3")); + assertThat(variant.gender, is(SpeechSynthesis.GENDER_UNSPECIFIED)); + assertThat(variant.age, is(SpeechSynthesis.AGE_OLD)); + assertThat(variant.toString(), is("klatt3-old")); + } +} diff --git a/android/icons/feature-graphic.svg b/android/icons/feature-graphic.svg new file mode 100644 index 00000000..30397d7b --- /dev/null +++ b/android/icons/feature-graphic.svg @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + eSpeak + for + Android + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/icons/launcher.svg b/android/icons/launcher.svg new file mode 100644 index 00000000..aa25d86f --- /dev/null +++ b/android/icons/launcher.svg @@ -0,0 +1,27 @@ + + + + + + + + + +eSpeak + + diff --git a/android/icons/promo-graphic.svg b/android/icons/promo-graphic.svg new file mode 100644 index 00000000..e82a6cee --- /dev/null +++ b/android/icons/promo-graphic.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + eSpeak + for + Android + + + + diff --git a/android/jni/Android.mk b/android/jni/Android.mk new file mode 100644 index 00000000..8d608107 --- /dev/null +++ b/android/jni/Android.mk @@ -0,0 +1,64 @@ +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 +UCDTOOLS_SRC_FILES := \ + $(subst $(LOCAL_PATH)/$(UCDTOOLS_SRC_PATH),$(UCDTOOLS_SRC_PATH),$(wildcard $(LOCAL_PATH)/$(UCDTOOLS_SRC_PATH)/*.c*)) + +LOCAL_SRC_FILES += $(UCDTOOLS_SRC_FILES) + +# eSpeak (minus command line apps and espeakedit) + +ESPEAK_SOURCES := \ + src/compiledict.cpp \ + src/debug.cpp \ + src/dictionary.cpp \ + src/intonation.cpp \ + src/klatt.cpp \ + src/numbers.cpp \ + src/readclause.cpp \ + src/phonemelist.cpp \ + src/setlengths.cpp \ + src/sonic.cpp \ + src/speak_lib.cpp \ + src/synthdata.cpp \ + src/synthesize.cpp \ + src/synth_mbrola.cpp \ + src/translate.cpp \ + src/tr_languages.cpp \ + src/voices.cpp \ + src/wavegen.cpp \ + src/wave.cpp + +ESPEAK_SRC_PATH := ../../src +ESPEAK_SRC_FILES := \ + $(subst src/,$(ESPEAK_SRC_PATH)/,$(ESPEAK_SOURCES)) + +LOCAL_CFLAGS += -DINCLUDE_KLATT -DINCLUDE_SONIC +LOCAL_SRC_FILES += \ + $(filter-out $(BLACKLIST_SRC_FILES),$(ESPEAK_SRC_FILES)) + +# JNI + +LOCAL_SRC_FILES += \ + $(subst $(LOCAL_PATH)/jni,jni,$(wildcard $(LOCAL_PATH)/jni/*.c*)) + +# Common + +LOCAL_C_INCLUDES += \ + $(LOCAL_PATH)/include \ + $(LOCAL_PATH)/$(UCDTOOLS_SRC_PATH)/include \ + $(LOCAL_PATH)/$(ESPEAK_SRC_PATH) + +LOCAL_LDLIBS := \ + -llog + +LOCAL_MODULE := libttsespeak +LOCAL_MODULE_TAGS := optional +LOCAL_PRELINK_MODULE := false + +include $(BUILD_SHARED_LIBRARY) diff --git a/android/jni/Application.mk b/android/jni/Application.mk new file mode 100644 index 00000000..d0767029 --- /dev/null +++ b/android/jni/Application.mk @@ -0,0 +1,4 @@ +APP_OPTIM := release +APP_PLATFORM := android-8 +APP_TOOLCHAIN_VERSION := 4.6.3 +APP_ABI := mips armeabi armeabi-v7a x86 diff --git a/android/jni/include/Log.h b/android/jni/include/Log.h new file mode 100644 index 00000000..982e4234 --- /dev/null +++ b/android/jni/include/Log.h @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2009 Google Inc. + * + * 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. + */ + +#ifndef LOG_H_ +#define LOG_H_ + +#include + +#define LOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, __VA_ARGS__) +#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__) +#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__) + +#endif /* LOG_H_ */ diff --git a/android/jni/jni/eSpeakService.cpp b/android/jni/jni/eSpeakService.cpp new file mode 100644 index 00000000..27fdb5c6 --- /dev/null +++ b/android/jni/jni/eSpeakService.cpp @@ -0,0 +1,354 @@ +/* + * Copyright (C) 2012-2015 Reece H. Dunn + * Copyright (C) 2011 Google Inc. + * + * 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. + */ + +/* + * This file contains the JNI bindings to eSpeak used by SpeechSynthesis.java. + * + * Android Version: 4.0 (Ice Cream Sandwich) + * API Version: 14 + */ + +#include +#include +#include +#include + +#include +#include + +#define BUFFER_SIZE_IN_MILLISECONDS 1000 + +/** @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) * sizeof(wchar_t)); + + 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 + +enum synthesis_result { + SYNTH_CONTINUE = 0, + SYNTH_ABORT = 1 +}; + +static JavaVM *jvm = NULL; +jmethodID METHOD_nativeSynthCallback; + +static JNIEnv *getJniEnv() { + JNIEnv *env = NULL; + jvm->AttachCurrentThread(&env, NULL); + return env; +} + +/* Callback from espeak. Should call back to the TTS API */ +static int SynthCallback(short *audioData, int numSamples, + espeak_EVENT *events) { + JNIEnv *env = getJniEnv(); + jobject object = (jobject)events->user_data; + + if (numSamples < 1) { + env->CallVoidMethod(object, METHOD_nativeSynthCallback, NULL); + return SYNTH_ABORT; + } else { + jbyteArray arrayAudioData = env->NewByteArray(numSamples * 2); + env->SetByteArrayRegion(arrayAudioData, 0, (numSamples * 2), (jbyte *) audioData); + env->CallVoidMethod(object, METHOD_nativeSynthCallback, arrayAudioData); + return SYNTH_CONTINUE; + } +} + +#ifdef __cplusplus +extern "C" { +#endif /* __cplusplus */ + +JNIEXPORT jint +JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { + jvm = vm; + JNIEnv *env; + + if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) { + LOGE("Failed to get the environment using GetEnv()"); + return -1; + } + + return JNI_VERSION_1_6; +} + +JNIEXPORT jboolean +JNICALL Java_com_reecedunn_espeak_SpeechSynthesis_nativeClassInit( + JNIEnv* env, jclass clazz) { + if (DEBUG) LOGV("%s", __FUNCTION__); + METHOD_nativeSynthCallback = env->GetMethodID(clazz, "nativeSynthCallback", "([B)V"); + + return JNI_TRUE; +} + +JNIEXPORT jint +JNICALL Java_com_reecedunn_espeak_SpeechSynthesis_nativeCreate( + JNIEnv *env, jobject object, jstring path) { + if (DEBUG) LOGV("%s [env=%p, object=%p]", __FUNCTION__, env, object); + + const char *c_path = path ? env->GetStringUTFChars(path, NULL) : NULL; + + if (DEBUG) LOGV("Initializing with path %s", c_path); + int sampleRate = espeak_Initialize(AUDIO_OUTPUT_SYNCHRONOUS, BUFFER_SIZE_IN_MILLISECONDS, c_path, 0); + + if (c_path) env->ReleaseStringUTFChars(path, c_path); + + return sampleRate; +} + +JNIEXPORT jobject +JNICALL Java_com_reecedunn_espeak_SpeechSynthesis_nativeGetVersion( + JNIEnv *env, jclass clazz) { + if (DEBUG) LOGV("%s", __FUNCTION__); + return env->NewStringUTF(espeak_Info(NULL)); +} + +JNIEXPORT jobjectArray +JNICALL Java_com_reecedunn_espeak_SpeechSynthesis_nativeGetAvailableVoices( + JNIEnv *env, jobject object) { + if (DEBUG) LOGV("%s", __FUNCTION__); + + const espeak_VOICE **voices = espeak_ListVoices(NULL); + + int count; + + // First, count the number of voices returned. + for (count = 0; voices[count] != NULL; count++); + + // Next, create a Java String array. + jobjectArray voicesArray = (jobjectArray) env->NewObjectArray( + count * 4, env->FindClass("java/lang/String"), NULL); + + const espeak_VOICE *v; + char gender_buf[12]; + char age_buf[12]; + + // Finally, populate the array. + for (int i = 0, voicesIndex = 0; (v = voices[i]) != NULL; i++) { + const char *lang_name = v->languages + 1; + const char *identifier = v->identifier; + sprintf(gender_buf, "%d", v->gender); + sprintf(age_buf, "%d", v->age); + + env->SetObjectArrayElement( + voicesArray, voicesIndex++, env->NewStringUTF(lang_name)); + env->SetObjectArrayElement( + voicesArray, voicesIndex++, env->NewStringUTF(identifier)); + env->SetObjectArrayElement( + voicesArray, voicesIndex++, env->NewStringUTF(gender_buf)); + env->SetObjectArrayElement( + voicesArray, voicesIndex++, env->NewStringUTF(age_buf)); + } + + return voicesArray; +} + +JNIEXPORT jboolean +JNICALL Java_com_reecedunn_espeak_SpeechSynthesis_nativeSetVoiceByName( + JNIEnv *env, jobject object, jstring name) { + const char *c_name = name ? env->GetStringUTFChars(name, NULL) : NULL; + + if (DEBUG) LOGV("%s(name=%s)", __FUNCTION__, c_name); + + const espeak_ERROR result = espeak_SetVoiceByName(c_name); + + if (c_name) env->ReleaseStringUTFChars(name, c_name); + + switch (result) { + case EE_OK: return JNI_TRUE; + case EE_INTERNAL_ERROR: LOGE("espeak_SetVoiceByName: internal error."); break; + case EE_BUFFER_FULL: LOGE("espeak_SetVoiceByName: buffer full."); break; + case EE_NOT_FOUND: LOGE("espeak_SetVoiceByName: not found."); break; + } + + return JNI_FALSE; +} + +JNIEXPORT jboolean +JNICALL Java_com_reecedunn_espeak_SpeechSynthesis_nativeSetVoiceByProperties( + JNIEnv *env, jobject object, jstring language, jint gender, jint age) { + const char *c_language = language ? env->GetStringUTFChars(language, NULL) : NULL; + + if (DEBUG) LOGV("%s(language=%s, gender=%d, age=%d)", __FUNCTION__, c_language, gender, age); + + espeak_VOICE voice_select; + memset(&voice_select, 0, sizeof(espeak_VOICE)); + voice_select.languages = c_language; + voice_select.gender = (int) gender; + voice_select.age = (int) age; + + const espeak_ERROR result = espeak_SetVoiceByProperties(&voice_select); + + if (c_language) env->ReleaseStringUTFChars(language, c_language); + + switch (result) { + case EE_OK: return JNI_TRUE; + case EE_INTERNAL_ERROR: LOGE("espeak_SetVoiceByProperties: internal error."); break; + case EE_BUFFER_FULL: LOGE("espeak_SetVoiceByProperties: buffer full."); break; + case EE_NOT_FOUND: LOGE("espeak_SetVoiceByProperties: not found."); break; + } + + return JNI_FALSE; +} + +JNIEXPORT jboolean +JNICALL Java_com_reecedunn_espeak_SpeechSynthesis_nativeSetParameter( + JNIEnv *env, jobject object, jint parameter, jint value) { + if (DEBUG) LOGV("%s(parameter=%d, value=%d)", __FUNCTION__, parameter, value); + const espeak_ERROR result = espeak_SetParameter((espeak_PARAMETER)parameter, (int)value, 0); + + switch (result) { + case EE_OK: return JNI_TRUE; + case EE_INTERNAL_ERROR: LOGE("espeak_SetParameter: internal error."); break; + case EE_BUFFER_FULL: LOGE("espeak_SetParameter: buffer full."); break; + case EE_NOT_FOUND: LOGE("espeak_SetParameter: not found."); break; + } + + return JNI_FALSE; +} + +JNIEXPORT jint +JNICALL Java_com_reecedunn_espeak_SpeechSynthesis_nativeGetParameter( + JNIEnv *env, jobject object, jint parameter, jint current) { + if (DEBUG) LOGV("%s(parameter=%d, pitch=%d)", __FUNCTION__, parameter, current); + 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) { + if (DEBUG) LOGV("%s", __FUNCTION__); + const char *c_text = text ? env->GetStringUTFChars(text, NULL) : NULL; + unsigned int unique_identifier; + + espeak_SetSynthCallback(SynthCallback); + const espeak_ERROR result = espeak_Synth(c_text, strlen(c_text), 0, // position + POS_CHARACTER, 0, // end position (0 means no end position) + isSsml ? espeakCHARS_UTF8 | espeakSSML // UTF-8 encoded SSML + : espeakCHARS_UTF8, // UTF-8 encoded text + &unique_identifier, object); + espeak_Synchronize(); + + if (c_text) env->ReleaseStringUTFChars(text, c_text); + + switch (result) { + case EE_OK: return JNI_TRUE; + case EE_INTERNAL_ERROR: LOGE("espeak_Synth: internal error."); break; + case EE_BUFFER_FULL: LOGE("espeak_Synth: buffer full."); break; + case EE_NOT_FOUND: LOGE("espeak_Synth: not found."); break; + } + + return JNI_TRUE; +} + +JNIEXPORT jboolean +JNICALL Java_com_reecedunn_espeak_SpeechSynthesis_nativeStop( + JNIEnv *env, jobject object) { + if (DEBUG) LOGV("%s", __FUNCTION__); + espeak_Cancel(); + + return JNI_TRUE; +} + +#ifdef __cplusplus +} +#endif /* __cplusplus */ diff --git a/android/mkicons.sh b/android/mkicons.sh new file mode 100755 index 00000000..bff6de7f --- /dev/null +++ b/android/mkicons.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +rsvg -w 24 -h 24 icons/launcher.svg res/drawable-ldpi/icon.png +rsvg -w 32 -h 32 icons/launcher.svg res/drawable-mdpi/icon.png +rsvg -w 48 -h 48 icons/launcher.svg res/drawable-hdpi/icon.png +rsvg -w 64 -h 64 icons/launcher.svg res/drawable-xhdpi/icon.png + +ls icons/Flag_*.svg | while read flag ; do + rsvg -w 75 -h 50 $flag `echo $flag | sed -e 's,.svg,.png,g'` +done + +rsvg -w 1024 -h 500 icons/feature-graphic.svg icons/feature-graphic.png +rsvg -w 180 -h 120 icons/promo-graphic.svg icons/promo-graphic.png diff --git a/android/project.properties b/android/project.properties new file mode 100644 index 00000000..73fc6610 --- /dev/null +++ b/android/project.properties @@ -0,0 +1,11 @@ +# This file is automatically generated by Android Tools. +# Do not modify this file -- YOUR CHANGES WILL BE ERASED! +# +# This file must be checked in Version Control Systems. +# +# To customize properties used by the Ant build system use, +# "ant.properties", and override values to adapt the script to your +# project structure. + +# Project target. +target=android-18 diff --git a/android/remove_string.sh b/android/remove_string.sh new file mode 100755 index 00000000..6ddc87e0 --- /dev/null +++ b/android/remove_string.sh @@ -0,0 +1,13 @@ +#!/bin/bash +# +# Copyright (C) 2013 Reece H. Dunn +# License: GPLv3+ +# +# Helper utility for removing a string/string-list item in all string resources. +# +# Usage: ./remove_string.sh + +ls res/values*/strings.xml | while read STRINGS ; do + xmlstarlet ed -P -d "/resources/*[@name='${1}']" ${STRINGS} > /tmp/strings.xml + mv /tmp/strings.xml ${STRINGS} +done diff --git a/android/res/drawable-hdpi/icon.png b/android/res/drawable-hdpi/icon.png new file mode 100644 index 00000000..dc2e72fa Binary files /dev/null and b/android/res/drawable-hdpi/icon.png differ diff --git a/android/res/drawable-ldpi/icon.png b/android/res/drawable-ldpi/icon.png new file mode 100644 index 00000000..09546b45 Binary files /dev/null and b/android/res/drawable-ldpi/icon.png differ diff --git a/android/res/drawable-mdpi/icon.png b/android/res/drawable-mdpi/icon.png new file mode 100644 index 00000000..9d70dcdb Binary files /dev/null and b/android/res/drawable-mdpi/icon.png differ diff --git a/android/res/drawable-xhdpi/icon.png b/android/res/drawable-xhdpi/icon.png new file mode 100644 index 00000000..dd5316ec Binary files /dev/null and b/android/res/drawable-xhdpi/icon.png differ diff --git a/android/res/layout/download_voice_data.xml b/android/res/layout/download_voice_data.xml new file mode 100644 index 00000000..b0db52a2 --- /dev/null +++ b/android/res/layout/download_voice_data.xml @@ -0,0 +1,24 @@ + + + + + + + + \ No newline at end of file diff --git a/android/res/layout/import_voice_preference.xml b/android/res/layout/import_voice_preference.xml new file mode 100644 index 00000000..dbd1daab --- /dev/null +++ b/android/res/layout/import_voice_preference.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/android/res/layout/information_view.xml b/android/res/layout/information_view.xml new file mode 100644 index 00000000..691e4904 --- /dev/null +++ b/android/res/layout/information_view.xml @@ -0,0 +1,27 @@ + + + + + + + + \ No newline at end of file diff --git a/android/res/layout/main.xml b/android/res/layout/main.xml new file mode 100644 index 00000000..da74dc3a --- /dev/null +++ b/android/res/layout/main.xml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + +