Add sample TTS engine
Bug: 4149751
Change-Id: Id8e96d4ec442db46cb97edaeab3ef7c13321ba1f
diff --git a/samples/TtsEngine/Android.mk b/samples/TtsEngine/Android.mk
new file mode 100644
index 0000000..30569ac
--- /dev/null
+++ b/samples/TtsEngine/Android.mk
@@ -0,0 +1,11 @@
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := samples
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_PACKAGE_NAME := TtsEngine
+
+include $(BUILD_PACKAGE)
+
diff --git a/samples/TtsEngine/AndroidManifest.xml b/samples/TtsEngine/AndroidManifest.xml
new file mode 100644
index 0000000..268ac43
--- /dev/null
+++ b/samples/TtsEngine/AndroidManifest.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 The Android Open Source Project
+
+ 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.example.android.ttsengine">
+
+ <!-- TODO: Fix this when the API level for ICS is finalized. -->
+ <uses-sdk android:minSdkVersion="IceCreamSandwich"
+ android:targetSdkVersion="IceCreamSandwich" />
+
+ <uses-permission android:name="android.permission.INTERNET" />
+
+ <application android:label="@string/app_name">
+ <service android:name=".RobotSpeakTtsService"
+ android:label="@string/app_name">
+ <intent-filter>
+ <action android:name="android.intent.action.TTS_SERVICE" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ <!-- Note that this meta data element must be inside the service
+ definition of the service that handles the TTS_SERVICE intent
+ -->
+ <meta-data android:name="android.speech.tts" android:resource="@xml/tts_engine" />
+ </service>
+
+ <activity android:name=".CheckVoiceData"
+ android:theme="@android:style/Theme.NoDisplay">
+ <intent-filter>
+ <action android:name="android.speech.tts.engine.CHECK_TTS_DATA" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
+
+ <activity android:name=".RobotSpeakSettings"
+ android:label="@string/general_settings" />
+
+ </application>
+
+</manifest>
+
diff --git a/samples/TtsEngine/_index.html b/samples/TtsEngine/_index.html
new file mode 100644
index 0000000..07a54a9
--- /dev/null
+++ b/samples/TtsEngine/_index.html
@@ -0,0 +1,30 @@
+<p>This sample demonstrates how to create a text to speech engine
+that users can install on their devices.</p>
+
+<p>The application includes a service and two activities:</p>
+<ul>
+ <li><a
+ href="src/com/example/android/ttsengine/RobotSpeakEngine.html"><code>RobotSpeakEngine</code></a>,
+ a simple text to speech engine that converts sentences into audio by
+ generating a square wave of a given frequency for each alphabet of a
+ given language. Though this doesn't qualify as speech (except for robots)
+ it exercises all aspects of the new text to speech API
+ by subclassing the
+ <a href="../../../reference/android/speech/tts/TextToSpeechService.html"><code>TextToSpeechService</code></a>
+ framework class.
+ </li>
+ <li><a
+ href="src/com/example/android/ttsengine/CheckVoiceData.html"><code>CheckVoiceData</code></a>,
+ an activity that checks that all voice related data is installed and
+ available.</li>
+ <li><a
+ href="src/com/example/android/ttsengine/RobotSpeakSettings.html"><code>RobotSpeakSettings</code></a>,
+ a settings screen for users to set various engine parameters. This is
+ usually accessed by users from the system wide settings app. This must be
+ declared in the <code>AndroidManifest.xml</code> file as a
+ <code>meta-data</code> element.</li>
+
+<!-- TODO: Fix this when the API level for ICS is finalized -->
+<p>Note that this API is supported only on Android 4.0 (API level 13)
+and higher versions of the platform.</p>
+
diff --git a/samples/TtsEngine/assets/eng-GBR.freq b/samples/TtsEngine/assets/eng-GBR.freq
new file mode 100644
index 0000000..61382ac
--- /dev/null
+++ b/samples/TtsEngine/assets/eng-GBR.freq
@@ -0,0 +1,26 @@
+z:2
+y:4
+x:8
+w:16
+v:32
+u:64
+t:128
+s:160
+r:320
+q:400
+p:640
+o:800
+n:1600
+m:3200
+l:4000
+k:8000
+j:20
+i:40
+h:80
+g:125
+f:250
+e:500
+d:1000
+c:2000
+b:5
+a:10
diff --git a/samples/TtsEngine/assets/eng-USA.freq b/samples/TtsEngine/assets/eng-USA.freq
new file mode 100644
index 0000000..9dfc147
--- /dev/null
+++ b/samples/TtsEngine/assets/eng-USA.freq
@@ -0,0 +1,26 @@
+a:2
+b:4
+c:8
+d:16
+e:32
+f:64
+g:128
+h:160
+i:320
+j:400
+k:640
+l:800
+m:1600
+n:3200
+o:4000
+p:8000
+q:20
+r:40
+s:80
+t:125
+u:250
+v:500
+w:1000
+x:2000
+y:5
+z:10
diff --git a/samples/TtsEngine/res/values/strings.xml b/samples/TtsEngine/res/values/strings.xml
new file mode 100644
index 0000000..70ad3fb
--- /dev/null
+++ b/samples/TtsEngine/res/values/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 The Android Open Source Project
+
+ 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.
+-->
+<resources>
+
+ <!-- The name of the application. -->
+ <string name="app_name">Example TTS Engine</string>
+ <!-- The heading of the first (and only) preference fragment -->
+ <string name="general_settings">General settings</string>
+
+ <string name="whisper_title">Whisper utterances</string>
+ <string name="whisper_summary">Say stuff really softly, only the keenest
+ eared robots can understand.</string>
+
+</resources>
diff --git a/samples/TtsEngine/res/xml/general_settings.xml b/samples/TtsEngine/res/xml/general_settings.xml
new file mode 100644
index 0000000..1cd6fd9
--- /dev/null
+++ b/samples/TtsEngine/res/xml/general_settings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 The Android Open Source Project
+
+ 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.
+-->
+<PreferenceScreen
+ xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <CheckboxPreference
+ android:key="robot_speak_whisper"
+ android:title="@string/whisper_title"
+ android:summary="@string/whisper_summary"
+ android:defaultValue="true"
+ android:persistent="true"
+ />
+
+</PreferenceScreen>
diff --git a/samples/TtsEngine/res/xml/preferences_headers.xml b/samples/TtsEngine/res/xml/preferences_headers.xml
new file mode 100644
index 0000000..53d0047
--- /dev/null
+++ b/samples/TtsEngine/res/xml/preferences_headers.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 The Android Open Source Project
+
+ 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.
+-->
+<preference-headers xmlns:android="http://schemas.android.com/apk/res/android">
+ <header android:fragment="com.example.android.ttsengine.GeneralSettingsFragment"
+ android:title="@string/general_settings"
+ />
+</preference-headers>
diff --git a/samples/TtsEngine/res/xml/tts_engine.xml b/samples/TtsEngine/res/xml/tts_engine.xml
new file mode 100644
index 0000000..fb6a823
--- /dev/null
+++ b/samples/TtsEngine/res/xml/tts_engine.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 The Android Open Source Project
+
+ 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.
+-->
+<tts-engine xmlns:android="http://schemas.android.com/apk/res/android"
+ android:settingsActivity="com.example.android.ttsengine.RobotSpeakSettings" />
diff --git a/samples/TtsEngine/src/com/example/android/ttsengine/CheckVoiceData.java b/samples/TtsEngine/src/com/example/android/ttsengine/CheckVoiceData.java
new file mode 100755
index 0000000..954dc80
--- /dev/null
+++ b/samples/TtsEngine/src/com/example/android/ttsengine/CheckVoiceData.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * 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.example.android.ttsengine;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.speech.tts.TextToSpeech;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/*
+ * Checks if the voice data is present.
+ */
+public class CheckVoiceData extends Activity {
+ private static final String TAG = "CheckVoiceData";
+
+ private static final String[] SUPPORTED_LANGUAGES = { "eng-GBR", "eng-USA" };
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ Intent intent = getIntent();
+ List<String> checkLanguages = getCheckVoiceDataFor(intent);
+
+ // If the call didn't specify which languages to check, check
+ // for all the supported ones.
+ if (checkLanguages.isEmpty()) {
+ checkLanguages = Arrays.asList(SUPPORTED_LANGUAGES);
+ }
+
+ ArrayList<String> available = new ArrayList<String>();
+ ArrayList<String> unavailable = new ArrayList<String>();
+
+ for (String lang : checkLanguages) {
+ // This check is required because checkLanguages might contain
+ // an arbitrary list of languages if the intent specified them
+ // {@link #getCheckVoiceDataFor}.
+ if (isLanguageSupported(lang)) {
+ if (isDataInstalled(lang)) {
+ available.add(lang);
+ } else {
+ unavailable.add(lang);
+ }
+ }
+ }
+
+ int result;
+ if (!checkLanguages.isEmpty() && available.isEmpty()) {
+ // No voices available at all.
+ result = TextToSpeech.Engine.CHECK_VOICE_DATA_FAIL;
+ } else if (!unavailable.isEmpty()) {
+ // Some voices are available, but some have missing
+ // data.
+ result = TextToSpeech.Engine.CHECK_VOICE_DATA_MISSING_DATA;
+ } else {
+ // All voices are available.
+ result = TextToSpeech.Engine.CHECK_VOICE_DATA_PASS;
+ }
+
+ // We now return the list of available and unavailable voices
+ // as well as the return code.
+ Intent returnData = new Intent();
+ returnData.putStringArrayListExtra(
+ TextToSpeech.Engine.EXTRA_AVAILABLE_VOICES, available);
+ returnData.putStringArrayListExtra(
+ TextToSpeech.Engine.EXTRA_UNAVAILABLE_VOICES, unavailable);
+ setResult(result, returnData);
+ finish();
+ }
+
+ /**
+ * The intent that launches this activity can contain an intent extra
+ * {@link TextToSpeech.Engine.EXTRA_CHECK_VOICE_DATA_FOR} that might specify
+ * a given language to check voice data for. If the intent does not contain
+ * this extra, we assume that a voice check for all supported languages
+ * was requested.
+ */
+ private List<String> getCheckVoiceDataFor(Intent intent) {
+ ArrayList<String> list = intent.getStringArrayListExtra(
+ TextToSpeech.Engine.EXTRA_CHECK_VOICE_DATA_FOR);
+ ArrayList<String> ret = new ArrayList<String>();
+ if (list != null) {
+ for (String lang : list) {
+ if (!TextUtils.isEmpty(lang)) {
+ ret.add(lang);
+ }
+ }
+ }
+ return ret;
+ }
+
+ /**
+ * Checks whether a given language is in the list of supported languages.
+ */
+ private boolean isLanguageSupported(String input) {
+ for (String lang : SUPPORTED_LANGUAGES) {
+ if (lang.equals(input)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /*
+ * Note that in our example, all data is packaged in our APK as
+ * assets (it could be a raw resource as well). This check is unnecessary
+ * because it will always succeed.
+ *
+ * If for example, engine data was downloaded or installed on external storage,
+ * this check would make much more sense.
+ */
+ private boolean isDataInstalled(String lang) {
+ try {
+ InputStream is = getAssets().open(lang + ".freq");
+
+ if (is != null) {
+ is.close();
+ } else {
+ return false;
+ }
+ } catch (IOException e) {
+ Log.w(TAG, "Unable to find data for: " + lang + ", exception: " + e);
+ return false;
+ }
+
+ // The asset InputStream was non null, and therefore this
+ // data file is available.
+ return true;
+ }
+}
diff --git a/samples/TtsEngine/src/com/example/android/ttsengine/GeneralSettingsFragment.java b/samples/TtsEngine/src/com/example/android/ttsengine/GeneralSettingsFragment.java
new file mode 100644
index 0000000..ca7353b
--- /dev/null
+++ b/samples/TtsEngine/src/com/example/android/ttsengine/GeneralSettingsFragment.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * 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.example.android.ttsengine;
+
+import android.os.Bundle;
+import android.preference.PreferenceFragment;
+
+public class GeneralSettingsFragment extends PreferenceFragment {
+ static final String SHARED_PREFS_NAME = "RobotSpeakSettings";
+ static final String WHISPER_KEY = "robot_speak_whisper";
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ getPreferenceManager().setSharedPreferencesName(SHARED_PREFS_NAME);
+ addPreferencesFromResource(R.xml.general_settings);
+ }
+}
diff --git a/samples/TtsEngine/src/com/example/android/ttsengine/RobotSpeakSettings.java b/samples/TtsEngine/src/com/example/android/ttsengine/RobotSpeakSettings.java
new file mode 100644
index 0000000..d4713d2
--- /dev/null
+++ b/samples/TtsEngine/src/com/example/android/ttsengine/RobotSpeakSettings.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * 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.example.android.ttsengine;
+
+import android.os.Bundle;
+import android.preference.PreferenceActivity;
+
+import java.util.List;
+
+/*
+ * This class is referenced via a meta data section in the manifest.
+ * A settings screen is optional, and if a given engine has no settings,
+ * there is no need to implement such a class.
+ */
+public class RobotSpeakSettings extends PreferenceActivity {
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ }
+
+ @Override
+ public void onBuildHeaders(List<Header> target) {
+ loadHeadersFromResource(R.xml.preferences_headers, target);
+ }
+}
diff --git a/samples/TtsEngine/src/com/example/android/ttsengine/RobotSpeakTtsService.java b/samples/TtsEngine/src/com/example/android/ttsengine/RobotSpeakTtsService.java
new file mode 100644
index 0000000..81f9f2b
--- /dev/null
+++ b/samples/TtsEngine/src/com/example/android/ttsengine/RobotSpeakTtsService.java
@@ -0,0 +1,294 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * 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.example.android.ttsengine;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.media.AudioFormat;
+import android.speech.tts.SynthesisCallback;
+import android.speech.tts.SynthesisRequest;
+import android.speech.tts.TextToSpeech;
+import android.speech.tts.TextToSpeechService;
+import android.util.Log;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A text to speech engine that generates "speech" that a robot might understand.
+ * The engine supports two different "languages", each with their own frequency
+ * mappings.
+ *
+ * It exercises all aspects of the Text to speech engine API
+ * {@link android.speech.tts.TextToSpeechService}.
+ */
+public class RobotSpeakTtsService extends TextToSpeechService {
+ private static final String TAG = "ExampleTtsService";
+
+ /*
+ * This is the sampling rate of our output audio. This engine outputs
+ * audio at 16khz 16bits per sample PCM audio.
+ */
+ private static final int SAMPLING_RATE_HZ = 16000;
+
+ /*
+ * We multiply by a factor of two since each sample contains 16 bits (2 bytes).
+ */
+ private final byte[] mAudioBuffer = new byte[SAMPLING_RATE_HZ * 2];
+
+ private Map<Character, Integer> mFrequenciesMap;
+ private volatile String[] mCurrentLanguage = null;
+ private volatile boolean mStopRequested = false;
+ private SharedPreferences mSharedPrefs = null;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ mSharedPrefs = getSharedPreferences(GeneralSettingsFragment.SHARED_PREFS_NAME,
+ Context.MODE_PRIVATE);
+ // We load the default language when we start up. This isn't strictly
+ // required though, it can always be loaded lazily on the first call to
+ // onLoadLanguage or onSynthesizeText. This a tradeoff between memory usage
+ // and the latency of the first call.
+ onLoadLanguage("eng", "usa", "");
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ }
+
+ @Override
+ protected String[] onGetLanguage() {
+ // Note that mCurrentLanguage is volatile because this can be called from
+ // multiple threads.
+ return mCurrentLanguage;
+ }
+
+ @Override
+ protected int onIsLanguageAvailable(String lang, String country, String variant) {
+ // The robot speak synthesizer supports only english.
+ if ("eng".equals(lang)) {
+ // We support two specific robot languages, the british robot language
+ // and the american robot language.
+ if ("USA".equals(country) || "GBR".equals(country)) {
+ // If the engine supported a specific variant, we would have
+ // something like.
+ //
+ // if ("android".equals(variant)) {
+ // return TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE;
+ // }
+ return TextToSpeech.LANG_COUNTRY_AVAILABLE;
+ }
+
+ // We support the language, but not the country.
+ return TextToSpeech.LANG_AVAILABLE;
+ }
+
+ return TextToSpeech.LANG_NOT_SUPPORTED;
+ }
+
+ /*
+ * Note that this method is synchronized, as is onSynthesizeText because
+ * onLoadLanguage can be called from multiple threads (while onSynthesizeText
+ * is always called from a single thread only).
+ */
+ @Override
+ protected synchronized int onLoadLanguage(String lang, String country, String variant) {
+ final int isLanguageAvailable = onIsLanguageAvailable(lang, country, variant);
+
+ if (isLanguageAvailable == TextToSpeech.LANG_NOT_SUPPORTED) {
+ return isLanguageAvailable;
+ }
+
+ String loadCountry = country;
+ if (isLanguageAvailable == TextToSpeech.LANG_AVAILABLE) {
+ loadCountry = "USA";
+ }
+
+ // If we've already loaded the requested language, we can return early.
+ if (mCurrentLanguage != null) {
+ if (mCurrentLanguage[0].equals(lang) && mCurrentLanguage[1].equals(country)) {
+ return isLanguageAvailable;
+ }
+ }
+
+ Map<Character, Integer> newFrequenciesMap = null;
+ try {
+ InputStream file = getAssets().open(lang + "-" + loadCountry + ".freq");
+ newFrequenciesMap = buildFrequencyMap(file);
+ file.close();
+ } catch (IOException e) {
+ Log.e(TAG, "Error loading data for : " + lang + "-" + country);
+ }
+
+ mFrequenciesMap = newFrequenciesMap;
+ mCurrentLanguage = new String[] { lang, loadCountry, ""};
+
+ return isLanguageAvailable;
+ }
+
+ @Override
+ protected void onStop() {
+ mStopRequested = true;
+ }
+
+ @Override
+ protected synchronized void onSynthesizeText(SynthesisRequest request,
+ SynthesisCallback callback) {
+ // Note that we call onLoadLanguage here since there is no guarantee
+ // that there would have been a prior call to this function.
+ int load = onLoadLanguage(request.getLanguage(), request.getCountry(),
+ request.getVariant());
+
+ // We might get requests for a language we don't support - in which case
+ // we error out early before wasting too much time.
+ if (load == TextToSpeech.LANG_NOT_SUPPORTED) {
+ callback.error();
+ return;
+ }
+
+ // At this point, we have loaded the language we need for synthesis and
+ // it is guaranteed that we support it so we proceed with synthesis.
+
+ // We denote that we are ready to start sending audio across to the
+ // framework. We use a fixed sampling rate (16khz), and send data across
+ // in 16bit PCM mono.
+ callback.start(SAMPLING_RATE_HZ,
+ AudioFormat.ENCODING_PCM_16BIT, 1 /* Number of channels. */);
+
+ // We then scan through each character of the request string and
+ // generate audio for it.
+ final String text = request.getText().toLowerCase();
+ for (int i = 0; i < text.length(); ++i) {
+ char value = normalize(text.charAt(i));
+ // It is crucial to call either of callback.error() or callback.done() to ensure
+ // that audio / other resources are released as soon as possible.
+ if (!generateOneSecondOfAudio(value, callback)) {
+ callback.error();
+ return;
+ }
+ }
+
+ // Alright, we're done with our synthesis - yay!
+ callback.done();
+ }
+
+ /*
+ * Normalizes a given character to the range 'a' - 'z' (inclusive). Our
+ * frequency mappings contain frequencies for each of these characters.
+ */
+ private static char normalize(char input) {
+ if (input == ' ') {
+ return input;
+ }
+
+ if (input < 'a') {
+ return 'a';
+ }
+ if (input > 'z') {
+ return 'z';
+ }
+
+ return input;
+ }
+
+ private Map<Character, Integer> buildFrequencyMap(InputStream is) throws IOException {
+ BufferedReader br = new BufferedReader(new InputStreamReader(is));
+ String line = null;
+ Map<Character, Integer> map = new HashMap<Character, Integer>();
+ try {
+ while ((line = br.readLine()) != null) {
+ String[] parts = line.split(":");
+ if (parts.length != 2) {
+ throw new IOException("Invalid line encountered: " + line);
+ }
+ map.put(parts[0].charAt(0), Integer.parseInt(parts[1]));
+ }
+ map.put(' ', 0);
+ return map;
+ } finally {
+ is.close();
+ }
+ }
+
+ private boolean generateOneSecondOfAudio(char alphabet, SynthesisCallback cb) {
+ ByteBuffer buffer = ByteBuffer.wrap(mAudioBuffer).order(ByteOrder.LITTLE_ENDIAN);
+
+ // Someone called onStop, end the current synthesis and return.
+ // The mStopRequested variable will be reset at the beginning of the
+ // next synthesis.
+ //
+ // In general, a call to onStop( ) should make a best effort attempt
+ // to stop all processing for the *current* onSynthesizeText request (if
+ // one is active).
+ if (mStopRequested) {
+ return false;
+ }
+
+
+ if (mFrequenciesMap == null || !mFrequenciesMap.containsKey(alphabet)) {
+ return false;
+ }
+
+ final int frequency = mFrequenciesMap.get(alphabet);
+
+ if (frequency > 0) {
+ // This is the wavelength in samples. The frequency is chosen so that the
+ // waveLength is always a multiple of two and frequency divides the
+ // SAMPLING_RATE exactly.
+ final int waveLength = SAMPLING_RATE_HZ / frequency;
+ final int times = SAMPLING_RATE_HZ / waveLength;
+
+ for (int j = 0; j < times; ++j) {
+ // For a square curve, half of the values will be at Short.MIN_VALUE
+ // and the other half will be Short.MAX_VALUE.
+ for (int i = 0; i < waveLength / 2; ++i) {
+ buffer.putShort((short)(getAmplitude() * -1));
+ }
+ for (int i = 0; i < waveLength / 2; ++i) {
+ buffer.putShort(getAmplitude());
+ }
+ }
+ } else {
+ // Play a second of silence.
+ for (int i = 0; i < mAudioBuffer.length / 2; ++i) {
+ buffer.putShort((short) 0);
+ }
+ }
+
+ // Get the maximum allowed size of data we can send across in audioAvailable.
+ final int maxBufferSize = cb.getMaxBufferSize();
+ int offset = 0;
+ while (offset < mAudioBuffer.length) {
+ int bytesToWrite = Math.min(maxBufferSize, mAudioBuffer.length - offset);
+ cb.audioAvailable(mAudioBuffer, offset, bytesToWrite);
+ offset += bytesToWrite;
+ }
+ return true;
+ }
+
+ private short getAmplitude() {
+ boolean whisper = mSharedPrefs.getBoolean(GeneralSettingsFragment.WHISPER_KEY, false);
+ return (short) (whisper ? 2048 : 8192);
+ }
+}