• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.
4 
5 package org.chromium.chrome.browser;
6 
7 import android.content.Context;
8 import android.speech.tts.TextToSpeech;
9 import android.speech.tts.UtteranceProgressListener;
10 
11 import org.chromium.base.CalledByNative;
12 import org.chromium.base.ThreadUtils;
13 
14 import java.util.ArrayList;
15 import java.util.HashMap;
16 import java.util.Locale;
17 
18 /**
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.
22  *
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++.
26  */
27 class TtsPlatformImpl {
28     private static class TtsVoice {
TtsVoice(String name, String language)29         private TtsVoice(String name, String language) {
30             mName = name;
31             mLanguage = language;
32         }
33         private final String mName;
34         private final String mLanguage;
35     }
36 
37     private static class PendingUtterance {
PendingUtterance(TtsPlatformImpl impl, int utteranceId, String text, String lang, float rate, float pitch, float volume)38         private PendingUtterance(TtsPlatformImpl impl, int utteranceId, String text,
39                 String lang, float rate, float pitch, float volume) {
40             mImpl = impl;
41             mUtteranceId = utteranceId;
42             mText = text;
43             mLang = lang;
44             mRate = rate;
45             mPitch = pitch;
46             mVolume = volume;
47         }
48 
speak()49         private void speak() {
50             mImpl.speak(mUtteranceId, mText, mLang, mRate, mPitch, mVolume);
51         }
52 
53         TtsPlatformImpl mImpl;
54         int mUtteranceId;
55         String mText;
56         String mLang;
57         float mRate;
58         float mPitch;
59         float mVolume;
60     }
61 
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;
68 
TtsPlatformImpl(long nativeTtsPlatformImplAndroid, Context context)69     private TtsPlatformImpl(long nativeTtsPlatformImplAndroid, Context context) {
70         mInitialized = false;
71         mNativeTtsPlatformImplAndroid = nativeTtsPlatformImplAndroid;
72         mTextToSpeech = new TextToSpeech(context, new TextToSpeech.OnInitListener() {
73                 @Override
74                 public void onInit(int status) {
75                     if (status == TextToSpeech.SUCCESS) {
76                         ThreadUtils.runOnUiThread(new Runnable() {
77                             @Override
78                             public void run() {
79                                 initialize();
80                             }
81                         });
82                     }
83                 }
84             });
85         mTextToSpeech.setOnUtteranceProgressListener(new UtteranceProgressListener() {
86                 @Override
87                 public void onDone(final String utteranceId) {
88                     ThreadUtils.runOnUiThread(new Runnable() {
89                         @Override
90                         public void run() {
91                             if (mNativeTtsPlatformImplAndroid != 0) {
92                                 nativeOnEndEvent(mNativeTtsPlatformImplAndroid,
93                                                  Integer.parseInt(utteranceId));
94                             }
95                         }
96                     });
97                 }
98 
99                 @Override
100                 public void onError(final String utteranceId) {
101                     ThreadUtils.runOnUiThread(new Runnable() {
102                         @Override
103                         public void run() {
104                             if (mNativeTtsPlatformImplAndroid != 0) {
105                                 nativeOnErrorEvent(mNativeTtsPlatformImplAndroid,
106                                                    Integer.parseInt(utteranceId));
107                             }
108                         }
109                     });
110                 }
111 
112                 @Override
113                 public void onStart(final String utteranceId) {
114                     ThreadUtils.runOnUiThread(new Runnable() {
115                         @Override
116                         public void run() {
117                             if (mNativeTtsPlatformImplAndroid != 0) {
118                                 nativeOnStartEvent(mNativeTtsPlatformImplAndroid,
119                                                    Integer.parseInt(utteranceId));
120                             }
121                         }
122                     });
123                 }
124             });
125     }
126 
127     /**
128      * Create a TtsPlatformImpl object, which is owned by TtsPlatformImplAndroid
129      * on the C++ side.
130      *
131      * @param nativeTtsPlatformImplAndroid The C++ object that owns us.
132      * @param context The app context.
133      */
134     @CalledByNative
create(long nativeTtsPlatformImplAndroid, Context context)135     private static TtsPlatformImpl create(long nativeTtsPlatformImplAndroid,
136                                           Context context) {
137         return new TtsPlatformImpl(nativeTtsPlatformImplAndroid, context);
138     }
139 
140     /**
141      * Called when our C++ counterpoint is deleted. Clear the handle to our
142      * native C++ object, ensuring it's never called.
143      */
144     @CalledByNative
destroy()145     private void destroy() {
146         mNativeTtsPlatformImplAndroid = 0;
147     }
148 
149     /**
150      * @return true if our TextToSpeech object is initialized and we've
151      * finished scanning the list of voices.
152      */
153     @CalledByNative
isInitialized()154     private boolean isInitialized() {
155         return mInitialized;
156     }
157 
158     /**
159      * @return the number of voices.
160      */
161     @CalledByNative
getVoiceCount()162     private int getVoiceCount() {
163         assert mInitialized == true;
164         return mVoices.size();
165     }
166 
167     /**
168      * @return the name of the voice at a given index.
169      */
170     @CalledByNative
getVoiceName(int voiceIndex)171     private String getVoiceName(int voiceIndex) {
172         assert mInitialized == true;
173         return mVoices.get(voiceIndex).mName;
174     }
175 
176     /**
177      * @return the language of the voice at a given index.
178      */
179     @CalledByNative
getVoiceLanguage(int voiceIndex)180     private String getVoiceLanguage(int voiceIndex) {
181         assert mInitialized == true;
182         return mVoices.get(voiceIndex).mLanguage;
183     }
184 
185     /**
186      * Attempt to start speaking an utterance. If it returns true, will call back on
187      * start and end.
188      *
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.
197      */
198     @CalledByNative
speak(int utteranceId, String text, String lang, float rate, float pitch, float volume)199     private boolean speak(int utteranceId, String text, String lang,
200                           float rate, float pitch, float volume) {
201         if (!mInitialized) {
202             mPendingUtterance = new PendingUtterance(this, utteranceId, text, lang, rate,
203                     pitch, volume);
204             return true;
205         }
206         if (mPendingUtterance != null) mPendingUtterance = null;
207 
208         if (!lang.equals(mCurrentLanguage)) {
209             mTextToSpeech.setLanguage(new Locale(lang));
210             mCurrentLanguage = lang;
211         }
212 
213         mTextToSpeech.setSpeechRate(rate);
214         mTextToSpeech.setPitch(pitch);
215         HashMap<String, String> params = new HashMap<String, String>();
216         if (volume != 1.0) {
217             params.put(TextToSpeech.Engine.KEY_PARAM_VOLUME, Double.toString(volume));
218         }
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);
222     }
223 
224     /**
225      * Stop the current utterance.
226      */
227     @CalledByNative
stop()228     private void stop() {
229         if (mInitialized) mTextToSpeech.stop();
230         if (mPendingUtterance != null) mPendingUtterance = null;
231     }
232 
233     /**
234      * Note: we enforce that this method is called on the UI thread, so
235      * we can call nativeVoicesChanged directly.
236      */
initialize()237     private void initialize() {
238         assert mNativeTtsPlatformImplAndroid != 0;
239 
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;
248         }
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             try {
254                 if (mTextToSpeech.isLanguageAvailable(locales[i]) > 0) {
255                     String name = locales[i].getDisplayLanguage();
256                     if (!locales[i].getCountry().isEmpty()) {
257                         name += " " + locales[i].getDisplayCountry();
258                     }
259                     TtsVoice voice = new TtsVoice(name, locales[i].toString());
260                     mVoices.add(voice);
261                 }
262             } catch (java.util.MissingResourceException e) {
263                 // Just skip the locale if it's invalid.
264             }
265         }
266 
267         mInitialized = true;
268         nativeVoicesChanged(mNativeTtsPlatformImplAndroid);
269 
270         if (mPendingUtterance != null) mPendingUtterance.speak();
271     }
272 
nativeVoicesChanged(long nativeTtsPlatformImplAndroid)273     private native void nativeVoicesChanged(long nativeTtsPlatformImplAndroid);
nativeOnEndEvent(long nativeTtsPlatformImplAndroid, int utteranceId)274     private native void nativeOnEndEvent(long nativeTtsPlatformImplAndroid, int utteranceId);
nativeOnStartEvent(long nativeTtsPlatformImplAndroid, int utteranceId)275     private native void nativeOnStartEvent(long nativeTtsPlatformImplAndroid, int utteranceId);
nativeOnErrorEvent(long nativeTtsPlatformImplAndroid, int utteranceId)276     private native void nativeOnErrorEvent(long nativeTtsPlatformImplAndroid, int utteranceId);
277 }
278