1 // Copyright 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 package org.chromium.chrome.browser;
7 import android.content.Context;
8 import android.speech.tts.TextToSpeech;
9 import android.speech.tts.UtteranceProgressListener;
11 import org.chromium.base.CalledByNative;
12 import org.chromium.base.ThreadUtils;
14 import java.util.ArrayList;
15 import java.util.HashMap;
16 import java.util.Locale;
19 * This class is the Java counterpart to the C++ TtsPlatformImplAndroid class.
20 * It implements the Android-native text-to-speech code to support the web
21 * speech synthesis API.
23 * Threading model note: all calls from C++ must happen on the UI thread.
24 * Callbacks from Android may happen on a different thread, so we always
25 * use ThreadUtils.runOnUiThread when calling back to C++.
27 class TtsPlatformImpl {
28 private static class TtsVoice {
29 private TtsVoice(String name, String language) {
33 private final String mName;
34 private final String mLanguage;
37 private static class PendingUtterance {
38 private PendingUtterance(TtsPlatformImpl impl, int utteranceId, String text,
39 String lang, float rate, float pitch, float volume) {
41 mUtteranceId = utteranceId;
49 private void speak() {
50 mImpl.speak(mUtteranceId, mText, mLang, mRate, mPitch, mVolume);
53 TtsPlatformImpl mImpl;
62 private long mNativeTtsPlatformImplAndroid;
63 private final TextToSpeech mTextToSpeech;
64 private boolean mInitialized;
65 private ArrayList<TtsVoice> mVoices;
66 private String mCurrentLanguage;
67 private PendingUtterance mPendingUtterance;
69 private TtsPlatformImpl(long nativeTtsPlatformImplAndroid, Context context) {
71 mNativeTtsPlatformImplAndroid = nativeTtsPlatformImplAndroid;
72 mTextToSpeech = new TextToSpeech(context, new TextToSpeech.OnInitListener() {
74 public void onInit(int status) {
75 if (status == TextToSpeech.SUCCESS) {
76 ThreadUtils.runOnUiThread(new Runnable() {
85 mTextToSpeech.setOnUtteranceProgressListener(new UtteranceProgressListener() {
87 public void onDone(final String utteranceId) {
88 ThreadUtils.runOnUiThread(new Runnable() {
91 if (mNativeTtsPlatformImplAndroid != 0) {
92 nativeOnEndEvent(mNativeTtsPlatformImplAndroid,
93 Integer.parseInt(utteranceId));
100 public void onError(final String utteranceId) {
101 ThreadUtils.runOnUiThread(new Runnable() {
104 if (mNativeTtsPlatformImplAndroid != 0) {
105 nativeOnErrorEvent(mNativeTtsPlatformImplAndroid,
106 Integer.parseInt(utteranceId));
113 public void onStart(final String utteranceId) {
114 ThreadUtils.runOnUiThread(new Runnable() {
117 if (mNativeTtsPlatformImplAndroid != 0) {
118 nativeOnStartEvent(mNativeTtsPlatformImplAndroid,
119 Integer.parseInt(utteranceId));
128 * Create a TtsPlatformImpl object, which is owned by TtsPlatformImplAndroid
131 * @param nativeTtsPlatformImplAndroid The C++ object that owns us.
132 * @param context The app context.
135 private static TtsPlatformImpl create(long nativeTtsPlatformImplAndroid,
137 return new TtsPlatformImpl(nativeTtsPlatformImplAndroid, context);
141 * Called when our C++ counterpoint is deleted. Clear the handle to our
142 * native C++ object, ensuring it's never called.
145 private void destroy() {
146 mNativeTtsPlatformImplAndroid = 0;
150 * @return true if our TextToSpeech object is initialized and we've
151 * finished scanning the list of voices.
154 private boolean isInitialized() {
159 * @return the number of voices.
162 private int getVoiceCount() {
163 assert mInitialized == true;
164 return mVoices.size();
168 * @return the name of the voice at a given index.
171 private String getVoiceName(int voiceIndex) {
172 assert mInitialized == true;
173 return mVoices.get(voiceIndex).mName;
177 * @return the language of the voice at a given index.
180 private String getVoiceLanguage(int voiceIndex) {
181 assert mInitialized == true;
182 return mVoices.get(voiceIndex).mLanguage;
186 * Attempt to start speaking an utterance. If it returns true, will call back on
189 * @param utteranceId A unique id for this utterance so that callbacks can be tied
190 * to a particular utterance.
191 * @param text The text to speak.
192 * @param lang The language code for the text (e.g., "en-US").
193 * @param rate The speech rate, in the units expected by Android TextToSpeech.
194 * @param pitch The speech pitch, in the units expected by Android TextToSpeech.
195 * @param volume The speech volume, in the units expected by Android TextToSpeech.
196 * @return true on success.
199 private boolean speak(int utteranceId, String text, String lang,
200 float rate, float pitch, float volume) {
202 mPendingUtterance = new PendingUtterance(this, utteranceId, text, lang, rate,
206 if (mPendingUtterance != null) mPendingUtterance = null;
208 if (!lang.equals(mCurrentLanguage)) {
209 mTextToSpeech.setLanguage(new Locale(lang));
210 mCurrentLanguage = lang;
213 mTextToSpeech.setSpeechRate(rate);
214 mTextToSpeech.setPitch(pitch);
215 HashMap<String, String> params = new HashMap<String, String>();
217 params.put(TextToSpeech.Engine.KEY_PARAM_VOLUME, Double.toString(volume));
219 params.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, Integer.toString(utteranceId));
220 int result = mTextToSpeech.speak(text, TextToSpeech.QUEUE_FLUSH, params);
221 return (result == TextToSpeech.SUCCESS);
225 * Stop the current utterance.
228 private void stop() {
229 if (mInitialized) mTextToSpeech.stop();
230 if (mPendingUtterance != null) mPendingUtterance = null;
234 * Note: we enforce that this method is called on the UI thread, so
235 * we can call nativeVoicesChanged directly.
237 private void initialize() {
238 assert mNativeTtsPlatformImplAndroid != 0;
240 // Note: Android supports multiple speech engines, but querying the
241 // metadata about all of them is expensive. So we deliberately only
242 // support the default speech engine, and expose the different
243 // supported languages for the default engine as different voices.
244 String defaultEngineName = mTextToSpeech.getDefaultEngine();
245 String engineLabel = defaultEngineName;
246 for (TextToSpeech.EngineInfo info : mTextToSpeech.getEngines()) {
247 if (info.name.equals(defaultEngineName)) engineLabel = info.label;
249 Locale[] locales = Locale.getAvailableLocales();
250 mVoices = new ArrayList<TtsVoice>();
251 for (int i = 0; i < locales.length; ++i) {
252 if (!locales[i].getVariant().isEmpty()) continue;
253 if (mTextToSpeech.isLanguageAvailable(locales[i]) > 0) {
254 String name = locales[i].getDisplayLanguage();
255 if (!locales[i].getCountry().isEmpty()) {
256 name += " " + locales[i].getDisplayCountry();
258 TtsVoice voice = new TtsVoice(name, locales[i].toString());
264 nativeVoicesChanged(mNativeTtsPlatformImplAndroid);
266 if (mPendingUtterance != null) mPendingUtterance.speak();
269 private native void nativeVoicesChanged(long nativeTtsPlatformImplAndroid);
270 private native void nativeOnEndEvent(long nativeTtsPlatformImplAndroid, int utteranceId);
271 private native void nativeOnStartEvent(long nativeTtsPlatformImplAndroid, int utteranceId);
272 private native void nativeOnErrorEvent(long nativeTtsPlatformImplAndroid, int utteranceId);