SpeechSynthesisService.java (7172B)
1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil -*- */ 2 /* vim: set ts=20 sts=4 et sw=4: */ 3 /* This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 package org.mozilla.gecko; 8 9 import android.content.Context; 10 import android.speech.tts.TextToSpeech; 11 import android.speech.tts.UtteranceProgressListener; 12 import android.util.Log; 13 import java.util.HashMap; 14 import java.util.HashSet; 15 import java.util.Locale; 16 import java.util.Set; 17 import java.util.UUID; 18 import java.util.concurrent.atomic.AtomicBoolean; 19 import org.mozilla.gecko.annotation.WrapForJNI; 20 import org.mozilla.gecko.util.ThreadUtils; 21 22 public class SpeechSynthesisService { 23 private static final String LOGTAG = "GeckoSpeechSynthesis"; 24 // Object type is used to make it easier to remove android.speech dependencies using Proguard. 25 private static Object sTTS; 26 27 @WrapForJNI(calledFrom = "gecko") 28 public static void initSynth() { 29 initSynthInternal(); 30 } 31 32 // Extra internal method to make it easier to remove android.speech dependencies using Proguard. 33 private static void initSynthInternal() { 34 if (sTTS != null) { 35 return; 36 } 37 38 final Context ctx = GeckoAppShell.getApplicationContext(); 39 40 sTTS = 41 new TextToSpeech( 42 ctx, 43 new TextToSpeech.OnInitListener() { 44 @Override 45 public void onInit(final int status) { 46 if (status != TextToSpeech.SUCCESS) { 47 Log.w(LOGTAG, "Failed to initialize TextToSpeech"); 48 return; 49 } 50 51 setUtteranceListener(); 52 registerVoicesByLocale(); 53 } 54 }); 55 } 56 57 private static TextToSpeech getTTS() { 58 return (TextToSpeech) sTTS; 59 } 60 61 private static void registerVoicesByLocale() { 62 ThreadUtils.postToBackgroundThread( 63 new Runnable() { 64 @Override 65 public void run() { 66 final TextToSpeech tss = getTTS(); 67 if (tss == null) { 68 Log.w(LOGTAG, "TextToSpeech is not initialized"); 69 return; 70 } 71 final Locale defaultLocale = tss.getDefaultLanguage(); 72 for (final Locale locale : getAvailableLanguages()) { 73 final Set<String> features = tss.getFeatures(locale); 74 final boolean isLocal = 75 features != null 76 && features.contains(TextToSpeech.Engine.KEY_FEATURE_EMBEDDED_SYNTHESIS); 77 final String localeStr = locale.toString(); 78 registerVoice( 79 "moz-tts:android:" + localeStr, 80 locale.getDisplayName(), 81 localeStr.replace("_", "-"), 82 !isLocal, 83 defaultLocale == locale); 84 } 85 doneRegisteringVoices(); 86 } 87 }); 88 } 89 90 private static Set<Locale> getAvailableLanguages() { 91 final Set<Locale> availableLanguages = getTTS().getAvailableLanguages(); 92 if (availableLanguages != null) { 93 return availableLanguages; 94 } 95 96 final Set<Locale> locales = new HashSet<Locale>(); 97 for (final Locale locale : Locale.getAvailableLocales()) { 98 if (locale.getVariant().isEmpty() && getTTS().isLanguageAvailable(locale) > 0) { 99 locales.add(locale); 100 } 101 } 102 103 return locales; 104 } 105 106 @WrapForJNI(dispatchTo = "gecko") 107 private static native void registerVoice( 108 String uri, String name, String locale, boolean isNetwork, boolean isDefault); 109 110 @WrapForJNI(dispatchTo = "gecko") 111 private static native void doneRegisteringVoices(); 112 113 @WrapForJNI(calledFrom = "gecko") 114 public static String speak( 115 final String uri, 116 final String text, 117 final float rate, 118 final float pitch, 119 final float volume) { 120 final AtomicBoolean result = new AtomicBoolean(false); 121 final String utteranceId = UUID.randomUUID().toString(); 122 speakInternal(uri, text, rate, pitch, volume, utteranceId, result); 123 return result.get() ? utteranceId : null; 124 } 125 126 // Extra internal method to make it easier to remove android.speech dependencies using Proguard. 127 private static void speakInternal( 128 final String uri, 129 final String text, 130 final float rate, 131 final float pitch, 132 final float volume, 133 final String utteranceId, 134 final AtomicBoolean result) { 135 if (sTTS == null) { 136 Log.w(LOGTAG, "TextToSpeech is not initialized"); 137 return; 138 } 139 140 final HashMap<String, String> params = new HashMap<String, String>(); 141 params.put(TextToSpeech.Engine.KEY_PARAM_VOLUME, Float.toString(volume)); 142 params.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, utteranceId); 143 final TextToSpeech tss = (TextToSpeech) sTTS; 144 tss.setLanguage(new Locale(uri.substring("moz-tts:android:".length()))); 145 tss.setSpeechRate(rate); 146 tss.setPitch(pitch); 147 final int speakRes = tss.speak(text, TextToSpeech.QUEUE_FLUSH, params); 148 result.set(speakRes == TextToSpeech.SUCCESS); 149 } 150 151 private static void setUtteranceListener() { 152 if (sTTS == null) { 153 Log.w(LOGTAG, "TextToSpeech is not initialized"); 154 return; 155 } 156 157 getTTS() 158 .setOnUtteranceProgressListener( 159 new UtteranceProgressListener() { 160 @Override 161 public void onDone(final String utteranceId) { 162 dispatchEnd(utteranceId); 163 } 164 165 @Override 166 public void onError(final String utteranceId) { 167 dispatchError(utteranceId); 168 } 169 170 @Override 171 public void onStart(final String utteranceId) { 172 dispatchStart(utteranceId); 173 } 174 175 @Override 176 public void onStop(final String utteranceId, final boolean interrupted) { 177 if (interrupted) { 178 dispatchEnd(utteranceId); 179 } else { 180 // utterance isn't started yet. 181 dispatchError(utteranceId); 182 } 183 } 184 185 public void onRangeStart( 186 final String utteranceId, final int start, final int end, final int frame) { 187 dispatchBoundary(utteranceId, start, end); 188 } 189 }); 190 } 191 192 @WrapForJNI(dispatchTo = "gecko") 193 private static native void dispatchStart(String utteranceId); 194 195 @WrapForJNI(dispatchTo = "gecko") 196 private static native void dispatchEnd(String utteranceId); 197 198 @WrapForJNI(dispatchTo = "gecko") 199 private static native void dispatchError(String utteranceId); 200 201 @WrapForJNI(dispatchTo = "gecko") 202 private static native void dispatchBoundary(String utteranceId, int start, int end); 203 204 @WrapForJNI(calledFrom = "gecko") 205 public static void stop() { 206 stopInternal(); 207 } 208 209 // Extra internal method to make it easier to remove android.speech dependencies using Proguard. 210 private static void stopInternal() { 211 if (sTTS == null) { 212 Log.w(LOGTAG, "TextToSpeech is not initialized"); 213 return; 214 } 215 216 getTTS().stop(); 217 } 218 }