Add sample TTS engine

Bug: 4149751
Change-Id: Id8e96d4ec442db46cb97edaeab3ef7c13321ba1f
This commit is contained in:
Narayan Kamath
2011-03-11 16:51:56 +00:00
parent e89c5e20a0
commit b688761bab
14 changed files with 753 additions and 0 deletions

View File

@@ -180,6 +180,7 @@ development/samples/Spinner samples/${PLATFORM_NAME}/Spinner
development/samples/SpinnerTest samples/${PLATFORM_NAME}/SpinnerTest development/samples/SpinnerTest samples/${PLATFORM_NAME}/SpinnerTest
development/samples/TicTacToeLib samples/${PLATFORM_NAME}/TicTacToeLib development/samples/TicTacToeLib samples/${PLATFORM_NAME}/TicTacToeLib
development/samples/TicTacToeMain samples/${PLATFORM_NAME}/TicTacToeMain development/samples/TicTacToeMain samples/${PLATFORM_NAME}/TicTacToeMain
development/samples/TtsEngine samples/${PLATFORM_NAME}/TtsEngine
development/samples/USB/MissileLauncher samples/${PLATFORM_NAME}/USB/MissileLauncher development/samples/USB/MissileLauncher samples/${PLATFORM_NAME}/USB/MissileLauncher
development/samples/USB/AdbTest samples/${PLATFORM_NAME}/USB/AdbTest development/samples/USB/AdbTest samples/${PLATFORM_NAME}/USB/AdbTest
development/samples/VoiceRecognitionService samples/${PLATFORM_NAME}/VoiceRecognitionService development/samples/VoiceRecognitionService samples/${PLATFORM_NAME}/VoiceRecognitionService

View File

@@ -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)

View File

@@ -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>

View File

@@ -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>

View File

@@ -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

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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" />

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}